From 76a7234cda1054eef86778834ccdac7a5655cad6 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 11 Jul 2012 09:55:22 +0800 Subject: [PATCH 001/195] Update: adding Combine, url mapping support. --- FileServer.js | 136 -- Main.js | 17 + MakeFile.list | 6 + StaticServer.js | 150 -- UrlMapper.js | 30 + ViewSvr.all.js | 249 +++ ViewSvr.js | 199 ++ build.bat | 6 + build_all.js | 304 +++ mime.js => lib/mime.js | 186 +- {types => lib/types}/mime.types | 3020 ++++++++++++++--------------- {types => lib/types}/node.types | 108 +- screenshot2.png => screenshot.png | Bin screenshot1.png | Bin 84448 -> 0 bytes testFileServer1.bat | 2 - testFileServer2.bat | 2 - testStaticServer.bat | 2 - tool/Combine.js | 183 ++ 18 files changed, 2651 insertions(+), 1949 deletions(-) delete mode 100644 FileServer.js create mode 100644 Main.js create mode 100644 MakeFile.list delete mode 100644 StaticServer.js create mode 100644 UrlMapper.js create mode 100644 ViewSvr.all.js create mode 100644 ViewSvr.js create mode 100644 build.bat create mode 100644 build_all.js rename mime.js => lib/mime.js (96%) rename {types => lib/types}/mime.types (97%) rename {types => lib/types}/node.types (96%) rename screenshot2.png => screenshot.png (100%) delete mode 100644 screenshot1.png delete mode 100644 testFileServer1.bat delete mode 100644 testFileServer2.bat delete mode 100644 testStaticServer.bat create mode 100644 tool/Combine.js diff --git a/FileServer.js b/FileServer.js deleted file mode 100644 index a4af073..0000000 --- a/FileServer.js +++ /dev/null @@ -1,136 +0,0 @@ -/* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, -* Author: Kris Zhang -* Blog: http://c52u.com -* Required: Node.js: http://www.nodejs.org. -* Date: 2012-3 Draft -*/ - -//FileServer Namespace -var FileServer; - -(function(){ - var fs = require("fs"), - path = require("path"); - - var server = FileServer = { - //Root path - dir: "C:\\Program Files", - //Listening port - port: 8021, - //How many files? - count: 0, - //Request object - request: null, - //Response object - response: null, - //List all the sub folders? - listSubFolder: true, - - //Initialize - init: function(dir, port, listSubFolder){ - server.dir = dir || server.dir; - server.port = Number(port) || server.port; - server.listSubFolder = listSubFolder == "true" && server.listSubFolder; - - var http = require("http"); - http.createServer(function(req, res){ - server.count = 0; - server.request = req; - server.response = res; - server.handle(req.url); - res.end(); - }).listen(server.port); - - console.log("Running at localhost", '\r' - , "dir:", server.dir, '\r' - , "Port:", server.port, '\r' - , "List Sub Folders:", server.listSubFolder, '\r' - ); - }, - - //Split the request, list folders or download file? - handle: function(url){ - var fullPath = path.join(server.dir, url); - try{ - stat = fs.statSync(fullPath) - }catch(err){ - server.response.writeHead(404, {"Content-Type": "text/html"}); - server.write("File not found!", err); - return; - } - - //Is file? Open this file and send to client. - if(stat.isFile()){ - var file = fs.readFileSync(fullPath); - server.response.writeHead(200, {"Content-Type": "application/octet-stream" }); - server.response.write(file, "binary"); - } - - //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ - var fullUrl = "http://" + server.request.headers.host + url; - - server.response.writeHead(200, {"Content-Type": "text/html"}); - server.write("

" + fullUrl + "


"); - server.write("
");
-				server.write(server.anchor("[To Parent Directory]", fullUrl.substr(0, fullUrl.lastIndexOf('/')), true) + "

"); - server.listFiles(fullPath, ""); - server.write("

"); - server.write("
Totoal files and directories: " + server.count + "
"); - } - }, - - //List all the files in a directory including the all the sub/child folders. - listFiles: function(dir, tab){ - var files = fs.readdirSync(dir); - - for(var idx in files){ - server.count++; - try{ - var filePath = path.join(dir, files[idx]), - stat = fs.statSync(filePath); - - server.write(tab - + server.anchor(files[idx], path.relative(server.dir, filePath)) - + "\r\n" - ); - - if(stat.isDirectory() && server.listSubFolder){ - server.listFiles(filePath, tab + "\t"); - } - } - catch(err){ - //TO DO: etc. permission not allowed - } - } - }, - - //Write contents to HttpResponse or Console - write: function(){ - if(server.response){ - for(var i in arguments){ - server.response.write(String(arguments[i])); - } - }else{ - console.log(arguments); - } - }, - - //Create an anchor - anchor: function(text, url, notFormat){ - if(!notFormat){ - url = '/' + url.replace('\\', '/'); - } - - return '' - + text - + ""; - }, - }; -})(); - -var fileServer = Object.create(FileServer); -fileServer.init(process.argv[2], process.argv[3], process.argv[4]); diff --git a/Main.js b/Main.js new file mode 100644 index 0000000..f6cba2a --- /dev/null +++ b/Main.js @@ -0,0 +1,17 @@ +/* +Main : start the server +*/ + +var urlMapper = new UrlMapper(); +var viewSvr = new ViewSvr("./../", 8000, urlMapper); +viewSvr.start(); + +/* +UrlMapper example: close server +http://localhost:8000/admin/close +*/ +urlMapper.add(/admin\/close/g, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + res.end("server is closed"); + viewSvr.close(); +}); \ No newline at end of file diff --git a/MakeFile.list b/MakeFile.list new file mode 100644 index 0000000..db12131 --- /dev/null +++ b/MakeFile.list @@ -0,0 +1,6 @@ +#config file +#These modules will be crunched to one. + +UrlMapper.js +ViewSvr.js +Main.js diff --git a/StaticServer.js b/StaticServer.js deleted file mode 100644 index 7c6634c..0000000 --- a/StaticServer.js +++ /dev/null @@ -1,150 +0,0 @@ -/* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, -* Author: Kris Zhang -* Blog: http://c52u.so -* Required: Node.js: http://www.nodejs.org, -* mime.js: https://github.com/bentomas/node-mime -* Date: 2012-3 Draft -* 2012-4 Update: Using async and mime.js -*/ - -//StaticServer Namespace -var StaticServer - -(function(){ - - StaticServer = function(strDir, strPort){ - var fs = require("fs"), - path = require("path"), - mime = require("./mime"), - //Root path - dir = "C:\\Program Files", - //Listening port - port = 8021, - //How many files? - count = 0; - - var urlFormat = function(url){ - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //align to right - var date = function(date){ - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //align to left - var size = function(num){ - return num + " ".substring(0, 12 - String(num).length); - }; - - var anchor = function(txt, url){ - url = url ? url : "/"; - return '' + txt + ""; - }; - - var requestHandler = function(request, response){ - count = 0; - - var url = request.url, - fullPath = path.join(dir, url), - stat; - try{ - stat = fs.statSync(fullPath) - }catch(err){ - response.writeHead(404, {"Content-Type": "text/html"}); - response.end("File not found!"); - return; - } - - //List all the files in a directory including the all the sub/child folders. - var listFiles = function(callback){ - - fs.readdir(fullPath, function(err, files){ - if(err){ - console.log(err); - return; - } - - for(var idx = 0, len = files.length; idx < len; idx++){ - //persitent the idx before make the sync process - (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - if(err){ - console.log(err); - return; - } - - count++; - - response.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - - idx == len -1 && callback(); - }); - })(idx); - } - }); - }; - - //Is file? Open this file and send to client. - if(stat.isFile()){ - fs.readFile(fullPath, function(err, data){ - if(err){ - console.log(err); - return; - } - response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); - response.end(data, "binary"); - }); - } - //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ - response.writeHead(200, {"Content-Type": "text/html"}); - response.write("

http://localhost:" + port + url + "


"); - response.write("
");
-				response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
-				listFiles(function(){
-					response.write("

"); - response.end("
Count: " + count + "
"); - }); - } - }; - - // Entry Point - (function(args){ - dir = args[2] || strDir || dir; - port = Number(args[3]) || Number(strPort) || port; - - try{ - //Create http server - require("http").createServer(requestHandler).listen(port); - - console.log("Running at localhost" - ,"dir:", dir - ,"Port:", port - ); - } - catch(err){ - console.log("Can't setup server at port", port, err); - } - })(process.argv); - }; -})(); - -new StaticServer(); \ No newline at end of file diff --git a/UrlMapper.js b/UrlMapper.js new file mode 100644 index 0000000..dbcb531 --- /dev/null +++ b/UrlMapper.js @@ -0,0 +1,30 @@ +/* +UrlMapper : mapping url; +*/ + +var UrlMapper = function(){ + var self = this, + maps = []; + + self.add = function(regExp, handler){ + maps.push({regExp: regExp, handler: handler}); + }; + + /* + Map the url + */ + self.match = function(req, res){ + for(var i = 0, len = maps.length; i < len ; i++){ + var mapper = maps[i]; + if(mapper.regExp && mapper.regExp.test(req.url)){ + try{ + mapper.handler && mapper.handler(req, res); + return true; + } + catch(err){ console.log(err) } + } + } + return false; + }; + +}; \ No newline at end of file diff --git a/ViewSvr.all.js b/ViewSvr.all.js new file mode 100644 index 0000000..bdfe337 --- /dev/null +++ b/ViewSvr.all.js @@ -0,0 +1,249 @@ +/*UrlMapper.js*/ +/* +UrlMapper : mapping url; +*/ + +var UrlMapper = function(){ + var self = this, + maps = []; + + self.add = function(regExp, handler){ + maps.push({regExp: regExp, handler: handler}); + }; + + /* + Map the url + */ + self.match = function(req, res){ + for(var i = 0, len = maps.length; i < len ; i++){ + var mapper = maps[i]; + if(mapper.regExp && mapper.regExp.test(req.url)){ + try{ + mapper.handler && mapper.handler(req, res); + return true; + } + catch(err){ console.log(err) } + } + } + return false; + }; + +}; +/*ViewSvr.js*/ +/* +* Description: Create a static file server (http based). +* This will list all the files and directories via Node.Js. +* The behavior will be like directory browsing enabled in IIS, +* Author: Kris Zhang +* Blog: http://c52u.so +* Required: Node.js: http://www.nodejs.org, +* mime.js: https://github.com/bentomas/node-mime +* Date: 2012-3 Draft +* 2012-4 Update: Using async and mime.js +* 2012-7 Update: Rename and reformat files +*/ +/* +* ViewSvr Namespace +*/ +var ViewSvr = (function(){ + + /* + var defaults = { + //root directory of the web + dir: "C:\\Program Files", + //listening port. + port: 8021, + //url mapping parameters. + urlMapper: null + }; + */ + + var server = function(strDir, strPort, urlMapper){ + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"), + //it self + self = this, + //Root path + dir = "C:\\Program Files", + //Listening port + port = 8021, + //How many files? + count = 0; + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + var anchor = function(txt, url){ + url = url ? url : "/"; + return '' + txt + ""; + }; + + var requestHandler = function(request, response){ + //url redirect module + if(urlMapper && urlMapper.match(request, response)){ + return; + } + + count = 0; + + var url = request.url; + + //bug can't recognized the parameter; + url = url.replace(/\?[\w_=-]+$/g, ''); + + var fullPath = path.join(dir, url), + stat; + + + try{ + stat = fs.statSync(fullPath) + }catch(err){ + response.writeHead(404, {"Content-Type": "text/html"}); + response.end("File not found!"); + return; + } + + //List all the files in a directory including the all the sub/child folders. + var listFiles = function(callback){ + + fs.readdir(fullPath, function(err, files){ + if(err){ + console.log(err); + return; + } + + for(var idx = 0, len = files.length; idx < len; idx++){ + //persitent the idx before make the sync process + (function(idx){ + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + if(err){ + console.log(err); + return; + } + + count++; + + response.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + + idx == len -1 && callback(); + }); + })(idx); + } + }); + }; + + //Is file? Open this file and send to client. + if(stat.isFile()){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); + response.end(data, "binary"); + }); + } + //Is Directory? List all the files and folders. + else if(stat.isDirectory()){ + response.writeHead(200, {"Content-Type": "text/html"}); + response.write("

http://localhost:" + port + url + "


"); + response.write("
");
+        response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
+        listFiles(function(){
+          response.write("

"); + response.end("
Count: " + count + "
"); + }); + } + }; + + /* + public start http server; + */ + self.start = function(){ + // Entry Point + (function(args){ + dir = args[2] || strDir || dir; + port = Number(args[3]) || Number(strPort) || port; + + try{ + //Create http server + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Running at localhost" + ,"dir:", dir + ,"Port:", port + ); + + self.httpSvr = httpSvr; + + return true; + } + catch(err){ + console.log("Can't setup server at port", port, err); + } + return false; + })(process.argv); + + }; + + /* + public close http server; + */ + self.close = function(){ + if(self.httpSvr){ + self.httpSvr.close(); + return true; + } + return false; + }; + + }; + + return server; + +})(); +/*Main.js*/ +/* +Main : start the server +*/ + +var urlMapper = new UrlMapper(); +var viewSvr = new ViewSvr("./../", 8000, urlMapper); +viewSvr.start(); + +/* +UrlMapper example: close server +http://localhost:8000/admin/close +*/ +urlMapper.add(/admin\/close/g, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + res.end("server is closed"); + viewSvr.close(); +}); diff --git a/ViewSvr.js b/ViewSvr.js new file mode 100644 index 0000000..c90c47b --- /dev/null +++ b/ViewSvr.js @@ -0,0 +1,199 @@ +/* +* Description: Create a static file server (http based). +* This will list all the files and directories via Node.Js. +* The behavior will be like directory browsing enabled in IIS, +* Author: Kris Zhang +* Blog: http://c52u.so +* Required: Node.js: http://www.nodejs.org, +* mime.js: https://github.com/bentomas/node-mime +* Date: 2012-3 Draft +* 2012-4 Update: Using async and mime.js +* 2012-7 Update: Rename and reformat files +*/ +/* +* ViewSvr Namespace +*/ +var ViewSvr = (function(){ + + /* + var defaults = { + //root directory of the web + dir: "C:\\Program Files", + //listening port. + port: 8021, + //url mapping parameters. + urlMapper: null + }; + */ + + var server = function(strDir, strPort, urlMapper){ + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"), + //it self + self = this, + //Root path + dir = "C:\\Program Files", + //Listening port + port = 8021, + //How many files? + count = 0; + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + var anchor = function(txt, url){ + url = url ? url : "/"; + return '' + txt + ""; + }; + + var requestHandler = function(request, response){ + //url redirect module + if(urlMapper && urlMapper.match(request, response)){ + return; + } + + count = 0; + + var url = request.url; + + //bug can't recognized the parameter; + url = url.replace(/\?[\w_=-]+$/g, ''); + + var fullPath = path.join(dir, url), + stat; + + + try{ + stat = fs.statSync(fullPath) + }catch(err){ + response.writeHead(404, {"Content-Type": "text/html"}); + response.end("File not found!"); + return; + } + + //List all the files in a directory including the all the sub/child folders. + var listFiles = function(callback){ + + fs.readdir(fullPath, function(err, files){ + if(err){ + console.log(err); + return; + } + + for(var idx = 0, len = files.length; idx < len; idx++){ + //persitent the idx before make the sync process + (function(idx){ + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + if(err){ + console.log(err); + return; + } + + count++; + + response.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + + idx == len -1 && callback(); + }); + })(idx); + } + }); + }; + + //Is file? Open this file and send to client. + if(stat.isFile()){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); + response.end(data, "binary"); + }); + } + //Is Directory? List all the files and folders. + else if(stat.isDirectory()){ + response.writeHead(200, {"Content-Type": "text/html"}); + response.write("

http://localhost:" + port + url + "


"); + response.write("
");
+        response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
+        listFiles(function(){
+          response.write("

"); + response.end("
Count: " + count + "
"); + }); + } + }; + + /* + public start http server; + */ + self.start = function(){ + // Entry Point + (function(args){ + dir = args[2] || strDir || dir; + port = Number(args[3]) || Number(strPort) || port; + + try{ + //Create http server + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Running at localhost" + ,"dir:", dir + ,"Port:", port + ); + + self.httpSvr = httpSvr; + + return true; + } + catch(err){ + console.log("Can't setup server at port", port, err); + } + return false; + })(process.argv); + + }; + + /* + public close http server; + */ + self.close = function(){ + if(self.httpSvr){ + self.httpSvr.close(); + return true; + } + return false; + }; + + }; + + return server; + +})(); \ No newline at end of file diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..3bc4526 --- /dev/null +++ b/build.bat @@ -0,0 +1,6 @@ +REM Combine first +node tool/Combine.js -i makefile.list -o ViewSvr.all.js +REM Then excute it +node ViewSvr.all.js +REM Complete Gooldbye. +pause; \ No newline at end of file diff --git a/build_all.js b/build_all.js new file mode 100644 index 0000000..c984d3e --- /dev/null +++ b/build_all.js @@ -0,0 +1,304 @@ +/*ViewSvr.js*/ +/* +* Description: Create a static file server (http based). +* This will list all the files and directories via Node.Js. +* The behavior will be like directory browsing enabled in IIS, +* Author: Kris Zhang +* Blog: http://c52u.so +* Required: Node.js: http://www.nodejs.org, +* mime.js: https://github.com/bentomas/node-mime +* Date: 2012-3 Draft +* 2012-4 Update: Using async and mime.js +* 2012-7 Update: Rename and reformat files +*/ +/* +* ViewSvr Namespace +*/ +var ViewSvr = (function(){ + + /* + var defaults = { + //root directory of the web + dir: "C:\\Program Files", + //listening port. + port: 8021, + //url mapping parameters. + urlMapper: null + }; + */ + + var server = function(strDir, strPort, urlMapper){ + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"), + //it self + self = this, + //Root path + dir = "C:\\Program Files", + //Listening port + port = 8021, + //How many files? + count = 0; + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + var anchor = function(txt, url){ + url = url ? url : "/"; + return '' + txt + ""; + }; + + var requestHandler = function(request, response){ + //url redirect module + if(urlMapper && urlMapper.match(request, response)){ + return; + } + + count = 0; + + var url = request.url; + + //bug can't recognized the parameter; + url = url.replace(/\?[\w_=-]+$/g, ''); + + var fullPath = path.join(dir, url), + stat; + + + try{ + stat = fs.statSync(fullPath) + }catch(err){ + response.writeHead(404, {"Content-Type": "text/html"}); + response.end("File not found!"); + return; + } + + //List all the files in a directory including the all the sub/child folders. + var listFiles = function(callback){ + + fs.readdir(fullPath, function(err, files){ + if(err){ + console.log(err); + return; + } + + for(var idx = 0, len = files.length; idx < len; idx++){ + //persitent the idx before make the sync process + (function(idx){ + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + if(err){ + console.log(err); + return; + } + + count++; + + response.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + + idx == len -1 && callback(); + }); + })(idx); + } + }); + }; + + //Is file? Open this file and send to client. + if(stat.isFile()){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); + response.end(data, "binary"); + }); + } + //Is Directory? List all the files and folders. + else if(stat.isDirectory()){ + response.writeHead(200, {"Content-Type": "text/html"}); + response.write("

http://localhost:" + port + url + "


"); + response.write("
");
+        response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
+        listFiles(function(){
+          response.write("

"); + response.end("
Count: " + count + "
"); + }); + } + }; + + /* + public start http server; + */ + self.start = function(){ + // Entry Point + (function(args){ + dir = args[2] || strDir || dir; + port = Number(args[3]) || Number(strPort) || port; + + try{ + //Create http server + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Running at localhost" + ,"dir:", dir + ,"Port:", port + ); + + self.httpSvr = httpSvr; + + return true; + } + catch(err){ + console.log("Can't setup server at port", port, err); + } + return false; + })(process.argv); + + }; + + /* + public close http server; + */ + self.close = function(){ + if(self.httpSvr){ + self.httpSvr.close(); + return true; + } + return false; + }; + + }; + + return server; + +})(); + +/*UrlMap.js*/ +var UrlMapper = function(){ + var self = this, + maps = []; + + self.add = function(regExp, handler){ + maps.push({regExp: regExp, handler: handler}); + }; + + /* + Map the url + */ + self.match = function(req, res){ + for(var i = 0, len = maps.length; i < len ; i++){ + var mapper = maps[i]; + if(mapper.regExp && mapper.regExp.test(req.url)){ + try{ + mapper.handler && mapper.handler(req, res); + return true; + } + catch(err){ console.log(err) } + } + } + return false; + }; + +}; + +/*Main.js*/ +var urlMapper = new UrlMapper(); +var viewSvr = new ViewSvr("./../", 8000, urlMapper); +viewSvr.start(); + +/* +UrlMapper example: close server +http://localhost:8000/admin/close +*/ +urlMapper.add(/admin\/close/g, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + res.end("server is closed"); + viewSvr.close(); +}); + + + + + + + + +var fs = require("fs"); + +urlMapper.add(/xhrlogin.htm/g, function(req, res){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.end('{"cookie":123456789}'); +}); + +urlMapper.add(/xhrlogin.htm/g, function(req, res){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.end('{"cookie":123456789}'); +}); + +urlMapper.add(/xhrindex.htm/g, function(req, res){ + var data = fs.readFileSync("D:\\www\\pduweb\\Web\\index.htm", "UTF-8"); + res.writeHead(200, {"Content-Type": "text/html" }); + res.end(data, "binary"); +}); + +urlMapper.add(/xhrmasterpg.htm/g, function(req, res){ + var data = fs.readFileSync("D:\\www\\pduweb\\Web\\masterpg.htm", "UTF-8"); + res.writeHead(200, {"Content-Type": "text/html" }); + res.end(data, "binary"); +}); + +urlMapper.add(/xhrdashboard.htm/g, function(req, res){ + var data = fs.readFileSync("D:\\www\\pduweb\\Web\\dashboar.htm", "UTF-8"); + res.writeHead(200, {"Content-Type": "text/html" }); + res.end(data, "binary"); +}); + +urlMapper.add(/xhrupdtfw.htm/g, function(req, res){ + var data = fs.readFileSync("D:\\www\\pduweb\\Web\\updtfw2.htm", "UTF-8"); + res.writeHead(200, {"Content-Type": "text/html" }); + res.end(data, "binary"); +}); + +urlMapper.add(/xhrfwfilepost.htm/g, function(req, res){ + + var formidable = require('./lib/incoming_form'); + + var form = new formidable.IncomingForm(); + form.parse(req, function(err, fields, files) { + if (err){ + console.log(err); + return; + }; + + //client.putFile("upload/"+files.upload.name, files.upload.name, function(err, res){} }); + res.writeHead(200, {'content-type': 'text/plain'}); + res.end(JSON.stringify({fields: fields, files: files})); + }); + +}); diff --git a/mime.js b/lib/mime.js similarity index 96% rename from mime.js rename to lib/mime.js index 64dbeec..28dcbfe 100644 --- a/mime.js +++ b/lib/mime.js @@ -1,93 +1,93 @@ -var path = require('path'), - fs = require('fs'); - -var mime = module.exports = { - // Map of extension to mime type - types: Object.create(null), - - // Map of mime type to extension - extensions :Object.create(null), - - /** - * Define mimetype -> extension mappings. Each key is a mime-type that maps - * to an array of extensions associated with the type. The first extension is - * used as the default extension for the type. - * - * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); - * - * @param map (Object) type definitions - */ - define: function(map) { - for (var type in map) { - var exts = map[type]; - - for (var i = 0; i < exts.length; i++) { - mime.types[exts[i]] = type; - } - - // Default extension is the first one we encounter - if (!mime.extensions[type]) { - mime.extensions[type] = exts[0]; - } - } - }, - - /** - * Load an Apache2-style ".types" file - * - * This may be called multiple times (it's expected). Where files declare - * overlapping types/extensions, the last file wins. - * - * @param file (String) path of file to load. - */ - load: function(file) { - // Read file and split into lines - var map = {}, - content = fs.readFileSync(file, 'ascii'), - lines = content.split(/[\r\n]+/); - - lines.forEach(function(line, lineno) { - // Clean up whitespace/comments, and split into fields - var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); - map[fields.shift()] = fields; - }); - - mime.define(map); - }, - - /** - * Lookup a mime type based on extension - */ - lookup: function(path, fallback) { - var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); - - return mime.types[ext] || fallback || mime.default_type - }, - - /** - * Return file extension associated with a mime type - */ - extension: function(mimeType) { - return mime.extensions[mimeType]; - }, - - /** - * Lookup a charset based on mime type. - */ - charsets: { - lookup: function (mimeType, fallback) { - // Assume text types are utf8. Modify mime logic as needed. - return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; - } - } -}; - -// Load our local copy of -// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -mime.load(path.join(__dirname, 'types/mime.types')); - -// Overlay enhancements submitted by the node.js community -mime.load(path.join(__dirname, 'types/node.types')); - -// Set the default type -mime.default_type = mime.types.bin; +var path = require('path'), + fs = require('fs'); + +var mime = module.exports = { + // Map of extension to mime type + types: Object.create(null), + + // Map of mime type to extension + extensions :Object.create(null), + + /** + * Define mimetype -> extension mappings. Each key is a mime-type that maps + * to an array of extensions associated with the type. The first extension is + * used as the default extension for the type. + * + * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); + * + * @param map (Object) type definitions + */ + define: function(map) { + for (var type in map) { + var exts = map[type]; + + for (var i = 0; i < exts.length; i++) { + mime.types[exts[i]] = type; + } + + // Default extension is the first one we encounter + if (!mime.extensions[type]) { + mime.extensions[type] = exts[0]; + } + } + }, + + /** + * Load an Apache2-style ".types" file + * + * This may be called multiple times (it's expected). Where files declare + * overlapping types/extensions, the last file wins. + * + * @param file (String) path of file to load. + */ + load: function(file) { + // Read file and split into lines + var map = {}, + content = fs.readFileSync(file, 'ascii'), + lines = content.split(/[\r\n]+/); + + lines.forEach(function(line, lineno) { + // Clean up whitespace/comments, and split into fields + var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); + map[fields.shift()] = fields; + }); + + mime.define(map); + }, + + /** + * Lookup a mime type based on extension + */ + lookup: function(path, fallback) { + var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); + + return mime.types[ext] || fallback || mime.default_type + }, + + /** + * Return file extension associated with a mime type + */ + extension: function(mimeType) { + return mime.extensions[mimeType]; + }, + + /** + * Lookup a charset based on mime type. + */ + charsets: { + lookup: function (mimeType, fallback) { + // Assume text types are utf8. Modify mime logic as needed. + return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; + } + } +}; + +// Load our local copy of +// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types +mime.load(path.join(__dirname, 'types/mime.types')); + +// Overlay enhancements submitted by the node.js community +mime.load(path.join(__dirname, 'types/node.types')); + +// Set the default type +mime.default_type = mime.types.bin; diff --git a/types/mime.types b/lib/types/mime.types similarity index 97% rename from types/mime.types rename to lib/types/mime.types index b3cae2e..cf9dbe8 100644 --- a/types/mime.types +++ b/lib/types/mime.types @@ -1,1510 +1,1510 @@ -# This file maps Internet media types to unique file extension(s). -# Although created for httpd, this file is used by many software systems -# and has been placed in the public domain for unlimited redisribution. -# -# The table below contains both registered and (common) unregistered types. -# A type that has no unique extension can be ignored -- they are listed -# here to guide configurations toward known types and to make it easier to -# identify "new" types. File extensions are also commonly used to indicate -# content languages and encodings, so choose them carefully. -# -# Internet media types should be registered as described in RFC 4288. -# The registry is at . -# -# MIME type (lowercased) Extensions -# ============================================ ========== -# application/1d-interleaved-parityfec -# application/3gpp-ims+xml -# application/activemessage -application/andrew-inset ez -# application/applefile -application/applixware aw -application/atom+xml atom -application/atomcat+xml atomcat -# application/atomicmail -application/atomsvc+xml atomsvc -# application/auth-policy+xml -# application/batch-smtp -# application/beep+xml -# application/calendar+xml -# application/cals-1840 -# application/ccmp+xml -application/ccxml+xml ccxml -application/cdmi-capability cdmia -application/cdmi-container cdmic -application/cdmi-domain cdmid -application/cdmi-object cdmio -application/cdmi-queue cdmiq -# application/cea-2018+xml -# application/cellml+xml -# application/cfw -# application/cnrp+xml -# application/commonground -# application/conference-info+xml -# application/cpl+xml -# application/csta+xml -# application/cstadata+xml -application/cu-seeme cu -# application/cybercash -application/davmount+xml davmount -# application/dca-rft -# application/dec-dx -# application/dialog-info+xml -# application/dicom -# application/dns -# application/dskpp+xml -application/dssc+der dssc -application/dssc+xml xdssc -# application/dvcs -application/ecmascript ecma -# application/edi-consent -# application/edi-x12 -# application/edifact -application/emma+xml emma -# application/epp+xml -application/epub+zip epub -# application/eshop -# application/example -application/exi exi -# application/fastinfoset -# application/fastsoap -# application/fits -application/font-tdpfr pfr -# application/framework-attributes+xml -# application/h224 -# application/held+xml -# application/http -application/hyperstudio stk -# application/ibe-key-request+xml -# application/ibe-pkg-reply+xml -# application/ibe-pp-data -# application/iges -# application/im-iscomposing+xml -# application/index -# application/index.cmd -# application/index.obj -# application/index.response -# application/index.vnd -application/inkml+xml ink inkml -# application/iotp -application/ipfix ipfix -# application/ipp -# application/isup -application/java-archive jar -application/java-serialized-object ser -application/java-vm class -application/javascript js -application/json json -# application/kpml-request+xml -# application/kpml-response+xml -application/lost+xml lostxml -application/mac-binhex40 hqx -application/mac-compactpro cpt -# application/macwriteii -application/mads+xml mads -application/marc mrc -application/marcxml+xml mrcx -application/mathematica ma nb mb -# application/mathml-content+xml -# application/mathml-presentation+xml -application/mathml+xml mathml -# application/mbms-associated-procedure-description+xml -# application/mbms-deregister+xml -# application/mbms-envelope+xml -# application/mbms-msk+xml -# application/mbms-msk-response+xml -# application/mbms-protection-description+xml -# application/mbms-reception-report+xml -# application/mbms-register+xml -# application/mbms-register-response+xml -# application/mbms-user-service-description+xml -application/mbox mbox -# application/media_control+xml -application/mediaservercontrol+xml mscml -application/metalink4+xml meta4 -application/mets+xml mets -# application/mikey -application/mods+xml mods -# application/moss-keys -# application/moss-signature -# application/mosskey-data -# application/mosskey-request -application/mp21 m21 mp21 -application/mp4 mp4s -# application/mpeg4-generic -# application/mpeg4-iod -# application/mpeg4-iod-xmt -# application/msc-ivr+xml -# application/msc-mixer+xml -application/msword doc dot -application/mxf mxf -# application/nasdata -# application/news-checkgroups -# application/news-groupinfo -# application/news-transmission -# application/nss -# application/ocsp-request -# application/ocsp-response -application/octet-stream bin dms lha lrf lzh so iso dmg dist distz pkg bpk dump elc deploy -application/oda oda -application/oebps-package+xml opf -application/ogg ogx -application/onenote onetoc onetoc2 onetmp onepkg -application/oxps oxps -# application/parityfec -application/patch-ops-error+xml xer -application/pdf pdf -application/pgp-encrypted pgp -# application/pgp-keys -application/pgp-signature asc sig -application/pics-rules prf -# application/pidf+xml -# application/pidf-diff+xml -application/pkcs10 p10 -application/pkcs7-mime p7m p7c -application/pkcs7-signature p7s -application/pkcs8 p8 -application/pkix-attr-cert ac -application/pkix-cert cer -application/pkix-crl crl -application/pkix-pkipath pkipath -application/pkixcmp pki -application/pls+xml pls -# application/poc-settings+xml -application/postscript ai eps ps -# application/prs.alvestrand.titrax-sheet -application/prs.cww cww -# application/prs.nprend -# application/prs.plucker -# application/prs.rdf-xml-crypt -# application/prs.xsf+xml -application/pskc+xml pskcxml -# application/qsig -application/rdf+xml rdf -application/reginfo+xml rif -application/relax-ng-compact-syntax rnc -# application/remote-printing -application/resource-lists+xml rl -application/resource-lists-diff+xml rld -# application/riscos -# application/rlmi+xml -application/rls-services+xml rs -application/rpki-ghostbusters gbr -application/rpki-manifest mft -application/rpki-roa roa -# application/rpki-updown -application/rsd+xml rsd -application/rss+xml rss -application/rtf rtf -# application/rtx -# application/samlassertion+xml -# application/samlmetadata+xml -application/sbml+xml sbml -application/scvp-cv-request scq -application/scvp-cv-response scs -application/scvp-vp-request spq -application/scvp-vp-response spp -application/sdp sdp -# application/set-payment -application/set-payment-initiation setpay -# application/set-registration -application/set-registration-initiation setreg -# application/sgml -# application/sgml-open-catalog -application/shf+xml shf -# application/sieve -# application/simple-filter+xml -# application/simple-message-summary -# application/simplesymbolcontainer -# application/slate -# application/smil -application/smil+xml smi smil -# application/soap+fastinfoset -# application/soap+xml -application/sparql-query rq -application/sparql-results+xml srx -# application/spirits-event+xml -application/srgs gram -application/srgs+xml grxml -application/sru+xml sru -application/ssml+xml ssml -# application/tamp-apex-update -# application/tamp-apex-update-confirm -# application/tamp-community-update -# application/tamp-community-update-confirm -# application/tamp-error -# application/tamp-sequence-adjust -# application/tamp-sequence-adjust-confirm -# application/tamp-status-query -# application/tamp-status-response -# application/tamp-update -# application/tamp-update-confirm -application/tei+xml tei teicorpus -application/thraud+xml tfi -# application/timestamp-query -# application/timestamp-reply -application/timestamped-data tsd -# application/tve-trigger -# application/ulpfec -# application/vcard+xml -# application/vemmi -# application/vividence.scriptfile -# application/vnd.3gpp.bsf+xml -application/vnd.3gpp.pic-bw-large plb -application/vnd.3gpp.pic-bw-small psb -application/vnd.3gpp.pic-bw-var pvb -# application/vnd.3gpp.sms -# application/vnd.3gpp2.bcmcsinfo+xml -# application/vnd.3gpp2.sms -application/vnd.3gpp2.tcap tcap -application/vnd.3m.post-it-notes pwn -application/vnd.accpac.simply.aso aso -application/vnd.accpac.simply.imp imp -application/vnd.acucobol acu -application/vnd.acucorp atc acutc -application/vnd.adobe.air-application-installer-package+zip air -application/vnd.adobe.fxp fxp fxpl -# application/vnd.adobe.partial-upload -application/vnd.adobe.xdp+xml xdp -application/vnd.adobe.xfdf xfdf -# application/vnd.aether.imp -# application/vnd.ah-barcode -application/vnd.ahead.space ahead -application/vnd.airzip.filesecure.azf azf -application/vnd.airzip.filesecure.azs azs -application/vnd.amazon.ebook azw -application/vnd.americandynamics.acc acc -application/vnd.amiga.ami ami -# application/vnd.amundsen.maze+xml -application/vnd.android.package-archive apk -application/vnd.anser-web-certificate-issue-initiation cii -application/vnd.anser-web-funds-transfer-initiation fti -application/vnd.antix.game-component atx -application/vnd.apple.installer+xml mpkg -application/vnd.apple.mpegurl m3u8 -# application/vnd.arastra.swi -application/vnd.aristanetworks.swi swi -application/vnd.astraea-software.iota iota -application/vnd.audiograph aep -# application/vnd.autopackage -# application/vnd.avistar+xml -application/vnd.blueice.multipass mpm -# application/vnd.bluetooth.ep.oob -application/vnd.bmi bmi -application/vnd.businessobjects rep -# application/vnd.cab-jscript -# application/vnd.canon-cpdl -# application/vnd.canon-lips -# application/vnd.cendio.thinlinc.clientconf -application/vnd.chemdraw+xml cdxml -application/vnd.chipnuts.karaoke-mmd mmd -application/vnd.cinderella cdy -# application/vnd.cirpack.isdn-ext -application/vnd.claymore cla -application/vnd.cloanto.rp9 rp9 -application/vnd.clonk.c4group c4g c4d c4f c4p c4u -application/vnd.cluetrust.cartomobile-config c11amc -application/vnd.cluetrust.cartomobile-config-pkg c11amz -# application/vnd.collection+json -# application/vnd.commerce-battelle -application/vnd.commonspace csp -application/vnd.contact.cmsg cdbcmsg -application/vnd.cosmocaller cmc -application/vnd.crick.clicker clkx -application/vnd.crick.clicker.keyboard clkk -application/vnd.crick.clicker.palette clkp -application/vnd.crick.clicker.template clkt -application/vnd.crick.clicker.wordbank clkw -application/vnd.criticaltools.wbs+xml wbs -application/vnd.ctc-posml pml -# application/vnd.ctct.ws+xml -# application/vnd.cups-pdf -# application/vnd.cups-postscript -application/vnd.cups-ppd ppd -# application/vnd.cups-raster -# application/vnd.cups-raw -# application/vnd.curl -application/vnd.curl.car car -application/vnd.curl.pcurl pcurl -# application/vnd.cybank -application/vnd.data-vision.rdz rdz -application/vnd.dece.data uvf uvvf uvd uvvd -application/vnd.dece.ttml+xml uvt uvvt -application/vnd.dece.unspecified uvx uvvx -application/vnd.dece.zip uvz uvvz -application/vnd.denovo.fcselayout-link fe_launch -# application/vnd.dir-bi.plate-dl-nosuffix -application/vnd.dna dna -application/vnd.dolby.mlp mlp -# application/vnd.dolby.mobile.1 -# application/vnd.dolby.mobile.2 -application/vnd.dpgraph dpg -application/vnd.dreamfactory dfac -application/vnd.dvb.ait ait -# application/vnd.dvb.dvbj -# application/vnd.dvb.esgcontainer -# application/vnd.dvb.ipdcdftnotifaccess -# application/vnd.dvb.ipdcesgaccess -# application/vnd.dvb.ipdcesgaccess2 -# application/vnd.dvb.ipdcesgpdd -# application/vnd.dvb.ipdcroaming -# application/vnd.dvb.iptv.alfec-base -# application/vnd.dvb.iptv.alfec-enhancement -# application/vnd.dvb.notif-aggregate-root+xml -# application/vnd.dvb.notif-container+xml -# application/vnd.dvb.notif-generic+xml -# application/vnd.dvb.notif-ia-msglist+xml -# application/vnd.dvb.notif-ia-registration-request+xml -# application/vnd.dvb.notif-ia-registration-response+xml -# application/vnd.dvb.notif-init+xml -# application/vnd.dvb.pfr -application/vnd.dvb.service svc -# application/vnd.dxr -application/vnd.dynageo geo -# application/vnd.easykaraoke.cdgdownload -# application/vnd.ecdis-update -application/vnd.ecowin.chart mag -# application/vnd.ecowin.filerequest -# application/vnd.ecowin.fileupdate -# application/vnd.ecowin.series -# application/vnd.ecowin.seriesrequest -# application/vnd.ecowin.seriesupdate -# application/vnd.emclient.accessrequest+xml -application/vnd.enliven nml -# application/vnd.eprints.data+xml -application/vnd.epson.esf esf -application/vnd.epson.msf msf -application/vnd.epson.quickanime qam -application/vnd.epson.salt slt -application/vnd.epson.ssf ssf -# application/vnd.ericsson.quickcall -application/vnd.eszigno3+xml es3 et3 -# application/vnd.etsi.aoc+xml -# application/vnd.etsi.cug+xml -# application/vnd.etsi.iptvcommand+xml -# application/vnd.etsi.iptvdiscovery+xml -# application/vnd.etsi.iptvprofile+xml -# application/vnd.etsi.iptvsad-bc+xml -# application/vnd.etsi.iptvsad-cod+xml -# application/vnd.etsi.iptvsad-npvr+xml -# application/vnd.etsi.iptvservice+xml -# application/vnd.etsi.iptvsync+xml -# application/vnd.etsi.iptvueprofile+xml -# application/vnd.etsi.mcid+xml -# application/vnd.etsi.overload-control-policy-dataset+xml -# application/vnd.etsi.sci+xml -# application/vnd.etsi.simservs+xml -# application/vnd.etsi.tsl+xml -# application/vnd.etsi.tsl.der -# application/vnd.eudora.data -application/vnd.ezpix-album ez2 -application/vnd.ezpix-package ez3 -# application/vnd.f-secure.mobile -application/vnd.fdf fdf -application/vnd.fdsn.mseed mseed -application/vnd.fdsn.seed seed dataless -# application/vnd.ffsns -# application/vnd.fints -application/vnd.flographit gph -application/vnd.fluxtime.clip ftc -# application/vnd.font-fontforge-sfd -application/vnd.framemaker fm frame maker book -application/vnd.frogans.fnc fnc -application/vnd.frogans.ltf ltf -application/vnd.fsc.weblaunch fsc -application/vnd.fujitsu.oasys oas -application/vnd.fujitsu.oasys2 oa2 -application/vnd.fujitsu.oasys3 oa3 -application/vnd.fujitsu.oasysgp fg5 -application/vnd.fujitsu.oasysprs bh2 -# application/vnd.fujixerox.art-ex -# application/vnd.fujixerox.art4 -# application/vnd.fujixerox.hbpl -application/vnd.fujixerox.ddd ddd -application/vnd.fujixerox.docuworks xdw -application/vnd.fujixerox.docuworks.binder xbd -# application/vnd.fut-misnet -application/vnd.fuzzysheet fzs -application/vnd.genomatix.tuxedo txd -# application/vnd.geocube+xml -application/vnd.geogebra.file ggb -application/vnd.geogebra.tool ggt -application/vnd.geometry-explorer gex gre -application/vnd.geonext gxt -application/vnd.geoplan g2w -application/vnd.geospace g3w -# application/vnd.globalplatform.card-content-mgt -# application/vnd.globalplatform.card-content-mgt-response -application/vnd.gmx gmx -application/vnd.google-earth.kml+xml kml -application/vnd.google-earth.kmz kmz -application/vnd.grafeq gqf gqs -# application/vnd.gridmp -application/vnd.groove-account gac -application/vnd.groove-help ghf -application/vnd.groove-identity-message gim -application/vnd.groove-injector grv -application/vnd.groove-tool-message gtm -application/vnd.groove-tool-template tpl -application/vnd.groove-vcard vcg -# application/vnd.hal+json -application/vnd.hal+xml hal -application/vnd.handheld-entertainment+xml zmm -application/vnd.hbci hbci -# application/vnd.hcl-bireports -application/vnd.hhe.lesson-player les -application/vnd.hp-hpgl hpgl -application/vnd.hp-hpid hpid -application/vnd.hp-hps hps -application/vnd.hp-jlyt jlt -application/vnd.hp-pcl pcl -application/vnd.hp-pclxl pclxl -# application/vnd.httphone -application/vnd.hydrostatix.sof-data sfd-hdstx -application/vnd.hzn-3d-crossword x3d -# application/vnd.ibm.afplinedata -# application/vnd.ibm.electronic-media -application/vnd.ibm.minipay mpy -application/vnd.ibm.modcap afp listafp list3820 -application/vnd.ibm.rights-management irm -application/vnd.ibm.secure-container sc -application/vnd.iccprofile icc icm -application/vnd.igloader igl -application/vnd.immervision-ivp ivp -application/vnd.immervision-ivu ivu -# application/vnd.informedcontrol.rms+xml -# application/vnd.informix-visionary -# application/vnd.infotech.project -# application/vnd.infotech.project+xml -application/vnd.insors.igm igm -application/vnd.intercon.formnet xpw xpx -application/vnd.intergeo i2g -# application/vnd.intertrust.digibox -# application/vnd.intertrust.nncp -application/vnd.intu.qbo qbo -application/vnd.intu.qfx qfx -# application/vnd.iptc.g2.conceptitem+xml -# application/vnd.iptc.g2.knowledgeitem+xml -# application/vnd.iptc.g2.newsitem+xml -# application/vnd.iptc.g2.packageitem+xml -application/vnd.ipunplugged.rcprofile rcprofile -application/vnd.irepository.package+xml irp -application/vnd.is-xpr xpr -application/vnd.isac.fcs fcs -application/vnd.jam jam -# application/vnd.japannet-directory-service -# application/vnd.japannet-jpnstore-wakeup -# application/vnd.japannet-payment-wakeup -# application/vnd.japannet-registration -# application/vnd.japannet-registration-wakeup -# application/vnd.japannet-setstore-wakeup -# application/vnd.japannet-verification -# application/vnd.japannet-verification-wakeup -application/vnd.jcp.javame.midlet-rms rms -application/vnd.jisp jisp -application/vnd.joost.joda-archive joda -application/vnd.kahootz ktz ktr -application/vnd.kde.karbon karbon -application/vnd.kde.kchart chrt -application/vnd.kde.kformula kfo -application/vnd.kde.kivio flw -application/vnd.kde.kontour kon -application/vnd.kde.kpresenter kpr kpt -application/vnd.kde.kspread ksp -application/vnd.kde.kword kwd kwt -application/vnd.kenameaapp htke -application/vnd.kidspiration kia -application/vnd.kinar kne knp -application/vnd.koan skp skd skt skm -application/vnd.kodak-descriptor sse -application/vnd.las.las+xml lasxml -# application/vnd.liberty-request+xml -application/vnd.llamagraphics.life-balance.desktop lbd -application/vnd.llamagraphics.life-balance.exchange+xml lbe -application/vnd.lotus-1-2-3 123 -application/vnd.lotus-approach apr -application/vnd.lotus-freelance pre -application/vnd.lotus-notes nsf -application/vnd.lotus-organizer org -application/vnd.lotus-screencam scm -application/vnd.lotus-wordpro lwp -application/vnd.macports.portpkg portpkg -# application/vnd.marlin.drm.actiontoken+xml -# application/vnd.marlin.drm.conftoken+xml -# application/vnd.marlin.drm.license+xml -# application/vnd.marlin.drm.mdcf -application/vnd.mcd mcd -application/vnd.medcalcdata mc1 -application/vnd.mediastation.cdkey cdkey -# application/vnd.meridian-slingshot -application/vnd.mfer mwf -application/vnd.mfmp mfm -application/vnd.micrografx.flo flo -application/vnd.micrografx.igx igx -application/vnd.mif mif -# application/vnd.minisoft-hp3000-save -# application/vnd.mitsubishi.misty-guard.trustweb -application/vnd.mobius.daf daf -application/vnd.mobius.dis dis -application/vnd.mobius.mbk mbk -application/vnd.mobius.mqy mqy -application/vnd.mobius.msl msl -application/vnd.mobius.plc plc -application/vnd.mobius.txf txf -application/vnd.mophun.application mpn -application/vnd.mophun.certificate mpc -# application/vnd.motorola.flexsuite -# application/vnd.motorola.flexsuite.adsi -# application/vnd.motorola.flexsuite.fis -# application/vnd.motorola.flexsuite.gotap -# application/vnd.motorola.flexsuite.kmr -# application/vnd.motorola.flexsuite.ttc -# application/vnd.motorola.flexsuite.wem -# application/vnd.motorola.iprm -application/vnd.mozilla.xul+xml xul -application/vnd.ms-artgalry cil -# application/vnd.ms-asf -application/vnd.ms-cab-compressed cab -application/vnd.ms-excel xls xlm xla xlc xlt xlw -application/vnd.ms-excel.addin.macroenabled.12 xlam -application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb -application/vnd.ms-excel.sheet.macroenabled.12 xlsm -application/vnd.ms-excel.template.macroenabled.12 xltm -application/vnd.ms-fontobject eot -application/vnd.ms-htmlhelp chm -application/vnd.ms-ims ims -application/vnd.ms-lrm lrm -# application/vnd.ms-office.activex+xml -application/vnd.ms-officetheme thmx -application/vnd.ms-pki.seccat cat -application/vnd.ms-pki.stl stl -# application/vnd.ms-playready.initiator+xml -application/vnd.ms-powerpoint ppt pps pot -application/vnd.ms-powerpoint.addin.macroenabled.12 ppam -application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm -application/vnd.ms-powerpoint.slide.macroenabled.12 sldm -application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm -application/vnd.ms-powerpoint.template.macroenabled.12 potm -application/vnd.ms-project mpp mpt -# application/vnd.ms-tnef -# application/vnd.ms-wmdrm.lic-chlg-req -# application/vnd.ms-wmdrm.lic-resp -# application/vnd.ms-wmdrm.meter-chlg-req -# application/vnd.ms-wmdrm.meter-resp -application/vnd.ms-word.document.macroenabled.12 docm -application/vnd.ms-word.template.macroenabled.12 dotm -application/vnd.ms-works wps wks wcm wdb -application/vnd.ms-wpl wpl -application/vnd.ms-xpsdocument xps -application/vnd.mseq mseq -# application/vnd.msign -# application/vnd.multiad.creator -# application/vnd.multiad.creator.cif -# application/vnd.music-niff -application/vnd.musician mus -application/vnd.muvee.style msty -application/vnd.mynfc taglet -# application/vnd.ncd.control -# application/vnd.ncd.reference -# application/vnd.nervana -# application/vnd.netfpx -application/vnd.neurolanguage.nlu nlu -application/vnd.noblenet-directory nnd -application/vnd.noblenet-sealer nns -application/vnd.noblenet-web nnw -# application/vnd.nokia.catalogs -# application/vnd.nokia.conml+wbxml -# application/vnd.nokia.conml+xml -# application/vnd.nokia.isds-radio-presets -# application/vnd.nokia.iptv.config+xml -# application/vnd.nokia.landmark+wbxml -# application/vnd.nokia.landmark+xml -# application/vnd.nokia.landmarkcollection+xml -# application/vnd.nokia.n-gage.ac+xml -application/vnd.nokia.n-gage.data ngdat -application/vnd.nokia.n-gage.symbian.install n-gage -# application/vnd.nokia.ncd -# application/vnd.nokia.pcd+wbxml -# application/vnd.nokia.pcd+xml -application/vnd.nokia.radio-preset rpst -application/vnd.nokia.radio-presets rpss -application/vnd.novadigm.edm edm -application/vnd.novadigm.edx edx -application/vnd.novadigm.ext ext -# application/vnd.ntt-local.file-transfer -# application/vnd.ntt-local.sip-ta_remote -# application/vnd.ntt-local.sip-ta_tcp_stream -application/vnd.oasis.opendocument.chart odc -application/vnd.oasis.opendocument.chart-template otc -application/vnd.oasis.opendocument.database odb -application/vnd.oasis.opendocument.formula odf -application/vnd.oasis.opendocument.formula-template odft -application/vnd.oasis.opendocument.graphics odg -application/vnd.oasis.opendocument.graphics-template otg -application/vnd.oasis.opendocument.image odi -application/vnd.oasis.opendocument.image-template oti -application/vnd.oasis.opendocument.presentation odp -application/vnd.oasis.opendocument.presentation-template otp -application/vnd.oasis.opendocument.spreadsheet ods -application/vnd.oasis.opendocument.spreadsheet-template ots -application/vnd.oasis.opendocument.text odt -application/vnd.oasis.opendocument.text-master odm -application/vnd.oasis.opendocument.text-template ott -application/vnd.oasis.opendocument.text-web oth -# application/vnd.obn -# application/vnd.oftn.l10n+json -# application/vnd.oipf.contentaccessdownload+xml -# application/vnd.oipf.contentaccessstreaming+xml -# application/vnd.oipf.cspg-hexbinary -# application/vnd.oipf.dae.svg+xml -# application/vnd.oipf.dae.xhtml+xml -# application/vnd.oipf.mippvcontrolmessage+xml -# application/vnd.oipf.pae.gem -# application/vnd.oipf.spdiscovery+xml -# application/vnd.oipf.spdlist+xml -# application/vnd.oipf.ueprofile+xml -# application/vnd.oipf.userprofile+xml -application/vnd.olpc-sugar xo -# application/vnd.oma-scws-config -# application/vnd.oma-scws-http-request -# application/vnd.oma-scws-http-response -# application/vnd.oma.bcast.associated-procedure-parameter+xml -# application/vnd.oma.bcast.drm-trigger+xml -# application/vnd.oma.bcast.imd+xml -# application/vnd.oma.bcast.ltkm -# application/vnd.oma.bcast.notification+xml -# application/vnd.oma.bcast.provisioningtrigger -# application/vnd.oma.bcast.sgboot -# application/vnd.oma.bcast.sgdd+xml -# application/vnd.oma.bcast.sgdu -# application/vnd.oma.bcast.simple-symbol-container -# application/vnd.oma.bcast.smartcard-trigger+xml -# application/vnd.oma.bcast.sprov+xml -# application/vnd.oma.bcast.stkm -# application/vnd.oma.cab-address-book+xml -# application/vnd.oma.cab-feature-handler+xml -# application/vnd.oma.cab-pcc+xml -# application/vnd.oma.cab-user-prefs+xml -# application/vnd.oma.dcd -# application/vnd.oma.dcdc -application/vnd.oma.dd2+xml dd2 -# application/vnd.oma.drm.risd+xml -# application/vnd.oma.group-usage-list+xml -# application/vnd.oma.pal+xml -# application/vnd.oma.poc.detailed-progress-report+xml -# application/vnd.oma.poc.final-report+xml -# application/vnd.oma.poc.groups+xml -# application/vnd.oma.poc.invocation-descriptor+xml -# application/vnd.oma.poc.optimized-progress-report+xml -# application/vnd.oma.push -# application/vnd.oma.scidm.messages+xml -# application/vnd.oma.xcap-directory+xml -# application/vnd.omads-email+xml -# application/vnd.omads-file+xml -# application/vnd.omads-folder+xml -# application/vnd.omaloc-supl-init -application/vnd.openofficeorg.extension oxt -# application/vnd.openxmlformats-officedocument.custom-properties+xml -# application/vnd.openxmlformats-officedocument.customxmlproperties+xml -# application/vnd.openxmlformats-officedocument.drawing+xml -# application/vnd.openxmlformats-officedocument.drawingml.chart+xml -# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml -# application/vnd.openxmlformats-officedocument.extended-properties+xml -# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml -# application/vnd.openxmlformats-officedocument.presentationml.comments+xml -# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml -application/vnd.openxmlformats-officedocument.presentationml.presentation pptx -# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml -application/vnd.openxmlformats-officedocument.presentationml.slide sldx -# application/vnd.openxmlformats-officedocument.presentationml.slide+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml -application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx -# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml -# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml -# application/vnd.openxmlformats-officedocument.presentationml.tags+xml -application/vnd.openxmlformats-officedocument.presentationml.template potx -# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx -# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml -# application/vnd.openxmlformats-officedocument.theme+xml -# application/vnd.openxmlformats-officedocument.themeoverride+xml -# application/vnd.openxmlformats-officedocument.vmldrawing -# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.document docx -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx -# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml -# application/vnd.openxmlformats-package.core-properties+xml -# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml -# application/vnd.openxmlformats-package.relationships+xml -# application/vnd.quobject-quoxdocument -# application/vnd.osa.netdeploy -application/vnd.osgeo.mapguide.package mgp -# application/vnd.osgi.bundle -application/vnd.osgi.dp dp -# application/vnd.otps.ct-kip+xml -application/vnd.palm pdb pqa oprc -# application/vnd.paos.xml -application/vnd.pawaafile paw -application/vnd.pg.format str -application/vnd.pg.osasli ei6 -# application/vnd.piaccess.application-licence -application/vnd.picsel efif -application/vnd.pmi.widget wg -# application/vnd.poc.group-advertisement+xml -application/vnd.pocketlearn plf -application/vnd.powerbuilder6 pbd -# application/vnd.powerbuilder6-s -# application/vnd.powerbuilder7 -# application/vnd.powerbuilder7-s -# application/vnd.powerbuilder75 -# application/vnd.powerbuilder75-s -# application/vnd.preminet -application/vnd.previewsystems.box box -application/vnd.proteus.magazine mgz -application/vnd.publishare-delta-tree qps -application/vnd.pvi.ptid1 ptid -# application/vnd.pwg-multiplexed -# application/vnd.pwg-xhtml-print+xml -# application/vnd.qualcomm.brew-app-res -application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb -# application/vnd.radisys.moml+xml -# application/vnd.radisys.msml+xml -# application/vnd.radisys.msml-audit+xml -# application/vnd.radisys.msml-audit-conf+xml -# application/vnd.radisys.msml-audit-conn+xml -# application/vnd.radisys.msml-audit-dialog+xml -# application/vnd.radisys.msml-audit-stream+xml -# application/vnd.radisys.msml-conf+xml -# application/vnd.radisys.msml-dialog+xml -# application/vnd.radisys.msml-dialog-base+xml -# application/vnd.radisys.msml-dialog-fax-detect+xml -# application/vnd.radisys.msml-dialog-fax-sendrecv+xml -# application/vnd.radisys.msml-dialog-group+xml -# application/vnd.radisys.msml-dialog-speech+xml -# application/vnd.radisys.msml-dialog-transform+xml -# application/vnd.rainstor.data -# application/vnd.rapid -application/vnd.realvnc.bed bed -application/vnd.recordare.musicxml mxl -application/vnd.recordare.musicxml+xml musicxml -# application/vnd.renlearn.rlprint -application/vnd.rig.cryptonote cryptonote -application/vnd.rim.cod cod -application/vnd.rn-realmedia rm -application/vnd.route66.link66+xml link66 -# application/vnd.ruckus.download -# application/vnd.s3sms -application/vnd.sailingtracker.track st -# application/vnd.sbm.cid -# application/vnd.sbm.mid2 -# application/vnd.scribus -# application/vnd.sealed.3df -# application/vnd.sealed.csf -# application/vnd.sealed.doc -# application/vnd.sealed.eml -# application/vnd.sealed.mht -# application/vnd.sealed.net -# application/vnd.sealed.ppt -# application/vnd.sealed.tiff -# application/vnd.sealed.xls -# application/vnd.sealedmedia.softseal.html -# application/vnd.sealedmedia.softseal.pdf -application/vnd.seemail see -application/vnd.sema sema -application/vnd.semd semd -application/vnd.semf semf -application/vnd.shana.informed.formdata ifm -application/vnd.shana.informed.formtemplate itp -application/vnd.shana.informed.interchange iif -application/vnd.shana.informed.package ipk -application/vnd.simtech-mindmapper twd twds -application/vnd.smaf mmf -# application/vnd.smart.notebook -application/vnd.smart.teacher teacher -# application/vnd.software602.filler.form+xml -# application/vnd.software602.filler.form-xml-zip -application/vnd.solent.sdkm+xml sdkm sdkd -application/vnd.spotfire.dxp dxp -application/vnd.spotfire.sfs sfs -# application/vnd.sss-cod -# application/vnd.sss-dtf -# application/vnd.sss-ntf -application/vnd.stardivision.calc sdc -application/vnd.stardivision.draw sda -application/vnd.stardivision.impress sdd -application/vnd.stardivision.math smf -application/vnd.stardivision.writer sdw vor -application/vnd.stardivision.writer-global sgl -application/vnd.stepmania.package smzip -application/vnd.stepmania.stepchart sm -# application/vnd.street-stream -application/vnd.sun.xml.calc sxc -application/vnd.sun.xml.calc.template stc -application/vnd.sun.xml.draw sxd -application/vnd.sun.xml.draw.template std -application/vnd.sun.xml.impress sxi -application/vnd.sun.xml.impress.template sti -application/vnd.sun.xml.math sxm -application/vnd.sun.xml.writer sxw -application/vnd.sun.xml.writer.global sxg -application/vnd.sun.xml.writer.template stw -# application/vnd.sun.wadl+xml -application/vnd.sus-calendar sus susp -application/vnd.svd svd -# application/vnd.swiftview-ics -application/vnd.symbian.install sis sisx -application/vnd.syncml+xml xsm -application/vnd.syncml.dm+wbxml bdm -application/vnd.syncml.dm+xml xdm -# application/vnd.syncml.dm.notification -# application/vnd.syncml.ds.notification -application/vnd.tao.intent-module-archive tao -application/vnd.tcpdump.pcap pcap cap dmp -application/vnd.tmobile-livetv tmo -application/vnd.trid.tpt tpt -application/vnd.triscape.mxs mxs -application/vnd.trueapp tra -# application/vnd.truedoc -# application/vnd.ubisoft.webplayer -application/vnd.ufdl ufd ufdl -application/vnd.uiq.theme utz -application/vnd.umajin umj -application/vnd.unity unityweb -application/vnd.uoml+xml uoml -# application/vnd.uplanet.alert -# application/vnd.uplanet.alert-wbxml -# application/vnd.uplanet.bearer-choice -# application/vnd.uplanet.bearer-choice-wbxml -# application/vnd.uplanet.cacheop -# application/vnd.uplanet.cacheop-wbxml -# application/vnd.uplanet.channel -# application/vnd.uplanet.channel-wbxml -# application/vnd.uplanet.list -# application/vnd.uplanet.list-wbxml -# application/vnd.uplanet.listcmd -# application/vnd.uplanet.listcmd-wbxml -# application/vnd.uplanet.signal -application/vnd.vcx vcx -# application/vnd.vd-study -# application/vnd.vectorworks -# application/vnd.verimatrix.vcas -# application/vnd.vidsoft.vidconference -application/vnd.visio vsd vst vss vsw -application/vnd.visionary vis -# application/vnd.vividence.scriptfile -application/vnd.vsf vsf -# application/vnd.wap.sic -# application/vnd.wap.slc -application/vnd.wap.wbxml wbxml -application/vnd.wap.wmlc wmlc -application/vnd.wap.wmlscriptc wmlsc -application/vnd.webturbo wtb -# application/vnd.wfa.wsc -# application/vnd.wmc -# application/vnd.wmf.bootstrap -# application/vnd.wolfram.mathematica -# application/vnd.wolfram.mathematica.package -application/vnd.wolfram.player nbp -application/vnd.wordperfect wpd -application/vnd.wqd wqd -# application/vnd.wrq-hp3000-labelled -application/vnd.wt.stf stf -# application/vnd.wv.csp+wbxml -# application/vnd.wv.csp+xml -# application/vnd.wv.ssp+xml -application/vnd.xara xar -application/vnd.xfdl xfdl -# application/vnd.xfdl.webform -# application/vnd.xmi+xml -# application/vnd.xmpie.cpkg -# application/vnd.xmpie.dpkg -# application/vnd.xmpie.plan -# application/vnd.xmpie.ppkg -# application/vnd.xmpie.xlim -application/vnd.yamaha.hv-dic hvd -application/vnd.yamaha.hv-script hvs -application/vnd.yamaha.hv-voice hvp -application/vnd.yamaha.openscoreformat osf -application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg -# application/vnd.yamaha.remote-setup -application/vnd.yamaha.smaf-audio saf -application/vnd.yamaha.smaf-phrase spf -# application/vnd.yamaha.through-ngn -# application/vnd.yamaha.tunnel-udpencap -application/vnd.yellowriver-custom-menu cmp -application/vnd.zul zir zirz -application/vnd.zzazz.deck+xml zaz -application/voicexml+xml vxml -# application/vq-rtcpxr -# application/watcherinfo+xml -# application/whoispp-query -# application/whoispp-response -application/widget wgt -application/winhlp hlp -# application/wita -# application/wordperfect5.1 -application/wsdl+xml wsdl -application/wspolicy+xml wspolicy -application/x-7z-compressed 7z -application/x-abiword abw -application/x-ace-compressed ace -application/x-authorware-bin aab x32 u32 vox -application/x-authorware-map aam -application/x-authorware-seg aas -application/x-bcpio bcpio -application/x-bittorrent torrent -application/x-bzip bz -application/x-bzip2 bz2 boz -application/x-cdlink vcd -application/x-chat chat -application/x-chess-pgn pgn -# application/x-compress -application/x-cpio cpio -application/x-csh csh -application/x-debian-package deb udeb -application/x-director dir dcr dxr cst cct cxt w3d fgd swa -application/x-doom wad -application/x-dtbncx+xml ncx -application/x-dtbook+xml dtb -application/x-dtbresource+xml res -application/x-dvi dvi -application/x-font-bdf bdf -# application/x-font-dos -# application/x-font-framemaker -application/x-font-ghostscript gsf -# application/x-font-libgrx -application/x-font-linux-psf psf -application/x-font-otf otf -application/x-font-pcf pcf -application/x-font-snf snf -# application/x-font-speedo -# application/x-font-sunos-news -application/x-font-ttf ttf ttc -application/x-font-type1 pfa pfb pfm afm -application/x-font-woff woff -# application/x-font-vfont -application/x-futuresplash spl -application/x-gnumeric gnumeric -application/x-gtar gtar -# application/x-gzip -application/x-hdf hdf -application/x-java-jnlp-file jnlp -application/x-latex latex -application/x-mobipocket-ebook prc mobi -application/x-ms-application application -application/x-ms-wmd wmd -application/x-ms-wmz wmz -application/x-ms-xbap xbap -application/x-msaccess mdb -application/x-msbinder obd -application/x-mscardfile crd -application/x-msclip clp -application/x-msdownload exe dll com bat msi -application/x-msmediaview mvb m13 m14 -application/x-msmetafile wmf -application/x-msmoney mny -application/x-mspublisher pub -application/x-msschedule scd -application/x-msterminal trm -application/x-mswrite wri -application/x-netcdf nc cdf -application/x-pkcs12 p12 pfx -application/x-pkcs7-certificates p7b spc -application/x-pkcs7-certreqresp p7r -application/x-rar-compressed rar -application/x-sh sh -application/x-shar shar -application/x-shockwave-flash swf -application/x-silverlight-app xap -application/x-stuffit sit -application/x-stuffitx sitx -application/x-sv4cpio sv4cpio -application/x-sv4crc sv4crc -application/x-tar tar -application/x-tcl tcl -application/x-tex tex -application/x-tex-tfm tfm -application/x-texinfo texinfo texi -application/x-ustar ustar -application/x-wais-source src -application/x-x509-ca-cert der crt -application/x-xfig fig -application/x-xpinstall xpi -# application/x400-bp -# application/xcap-att+xml -# application/xcap-caps+xml -application/xcap-diff+xml xdf -# application/xcap-el+xml -# application/xcap-error+xml -# application/xcap-ns+xml -# application/xcon-conference-info-diff+xml -# application/xcon-conference-info+xml -application/xenc+xml xenc -application/xhtml+xml xhtml xht -# application/xhtml-voice+xml -application/xml xml xsl -application/xml-dtd dtd -# application/xml-external-parsed-entity -# application/xmpp+xml -application/xop+xml xop -application/xslt+xml xslt -application/xspf+xml xspf -application/xv+xml mxml xhvml xvml xvm -application/yang yang -application/yin+xml yin -application/zip zip -# audio/1d-interleaved-parityfec -# audio/32kadpcm -# audio/3gpp -# audio/3gpp2 -# audio/ac3 -audio/adpcm adp -# audio/amr -# audio/amr-wb -# audio/amr-wb+ -# audio/asc -# audio/atrac-advanced-lossless -# audio/atrac-x -# audio/atrac3 -audio/basic au snd -# audio/bv16 -# audio/bv32 -# audio/clearmode -# audio/cn -# audio/dat12 -# audio/dls -# audio/dsr-es201108 -# audio/dsr-es202050 -# audio/dsr-es202211 -# audio/dsr-es202212 -# audio/dv -# audio/dvi4 -# audio/eac3 -# audio/evrc -# audio/evrc-qcp -# audio/evrc0 -# audio/evrc1 -# audio/evrcb -# audio/evrcb0 -# audio/evrcb1 -# audio/evrcwb -# audio/evrcwb0 -# audio/evrcwb1 -# audio/example -# audio/fwdred -# audio/g719 -# audio/g722 -# audio/g7221 -# audio/g723 -# audio/g726-16 -# audio/g726-24 -# audio/g726-32 -# audio/g726-40 -# audio/g728 -# audio/g729 -# audio/g7291 -# audio/g729d -# audio/g729e -# audio/gsm -# audio/gsm-efr -# audio/gsm-hr-08 -# audio/ilbc -# audio/ip-mr_v2.5 -# audio/l16 -# audio/l20 -# audio/l24 -# audio/l8 -# audio/lpc -audio/midi mid midi kar rmi -# audio/mobile-xmf -audio/mp4 mp4a -# audio/mp4a-latm -# audio/mpa -# audio/mpa-robust -audio/mpeg mpga mp2 mp2a mp3 m2a m3a -# audio/mpeg4-generic -audio/ogg oga ogg spx -# audio/parityfec -# audio/pcma -# audio/pcma-wb -# audio/pcmu-wb -# audio/pcmu -# audio/prs.sid -# audio/qcelp -# audio/red -# audio/rtp-enc-aescm128 -# audio/rtp-midi -# audio/rtx -# audio/smv -# audio/smv0 -# audio/smv-qcp -# audio/sp-midi -# audio/speex -# audio/t140c -# audio/t38 -# audio/telephone-event -# audio/tone -# audio/uemclip -# audio/ulpfec -# audio/vdvi -# audio/vmr-wb -# audio/vnd.3gpp.iufp -# audio/vnd.4sb -# audio/vnd.audiokoz -# audio/vnd.celp -# audio/vnd.cisco.nse -# audio/vnd.cmles.radio-events -# audio/vnd.cns.anp1 -# audio/vnd.cns.inf1 -audio/vnd.dece.audio uva uvva -audio/vnd.digital-winds eol -# audio/vnd.dlna.adts -# audio/vnd.dolby.heaac.1 -# audio/vnd.dolby.heaac.2 -# audio/vnd.dolby.mlp -# audio/vnd.dolby.mps -# audio/vnd.dolby.pl2 -# audio/vnd.dolby.pl2x -# audio/vnd.dolby.pl2z -# audio/vnd.dolby.pulse.1 -audio/vnd.dra dra -audio/vnd.dts dts -audio/vnd.dts.hd dtshd -# audio/vnd.dvb.file dvb -# audio/vnd.everad.plj -# audio/vnd.hns.audio -audio/vnd.lucent.voice lvp -audio/vnd.ms-playready.media.pya pya -# audio/vnd.nokia.mobile-xmf -# audio/vnd.nortel.vbk -audio/vnd.nuera.ecelp4800 ecelp4800 -audio/vnd.nuera.ecelp7470 ecelp7470 -audio/vnd.nuera.ecelp9600 ecelp9600 -# audio/vnd.octel.sbc -# audio/vnd.qcelp -# audio/vnd.rhetorex.32kadpcm -audio/vnd.rip rip -# audio/vnd.sealedmedia.softseal.mpeg -# audio/vnd.vmx.cvsd -# audio/vorbis -# audio/vorbis-config -audio/webm weba -audio/x-aac aac -audio/x-aiff aif aiff aifc -audio/x-mpegurl m3u -audio/x-ms-wax wax -audio/x-ms-wma wma -audio/x-pn-realaudio ram ra -audio/x-pn-realaudio-plugin rmp -audio/x-wav wav -chemical/x-cdx cdx -chemical/x-cif cif -chemical/x-cmdf cmdf -chemical/x-cml cml -chemical/x-csml csml -# chemical/x-pdb -chemical/x-xyz xyz -image/bmp bmp -image/cgm cgm -# image/example -# image/fits -image/g3fax g3 -image/gif gif -image/ief ief -# image/jp2 -image/jpeg jpeg jpg jpe -# image/jpm -# image/jpx -image/ktx ktx -# image/naplps -image/png png -image/prs.btif btif -# image/prs.pti -image/svg+xml svg svgz -# image/t38 -image/tiff tiff tif -# image/tiff-fx -image/vnd.adobe.photoshop psd -# image/vnd.cns.inf2 -image/vnd.dece.graphic uvi uvvi uvg uvvg -image/vnd.dvb.subtitle sub -image/vnd.djvu djvu djv -image/vnd.dwg dwg -image/vnd.dxf dxf -image/vnd.fastbidsheet fbs -image/vnd.fpx fpx -image/vnd.fst fst -image/vnd.fujixerox.edmics-mmr mmr -image/vnd.fujixerox.edmics-rlc rlc -# image/vnd.globalgraphics.pgb -# image/vnd.microsoft.icon -# image/vnd.mix -image/vnd.ms-modi mdi -image/vnd.net-fpx npx -# image/vnd.radiance -# image/vnd.sealed.png -# image/vnd.sealedmedia.softseal.gif -# image/vnd.sealedmedia.softseal.jpg -# image/vnd.svf -image/vnd.wap.wbmp wbmp -image/vnd.xiff xif -image/webp webp -image/x-cmu-raster ras -image/x-cmx cmx -image/x-freehand fh fhc fh4 fh5 fh7 -image/x-icon ico -image/x-pcx pcx -image/x-pict pic pct -image/x-portable-anymap pnm -image/x-portable-bitmap pbm -image/x-portable-graymap pgm -image/x-portable-pixmap ppm -image/x-rgb rgb -image/x-xbitmap xbm -image/x-xpixmap xpm -image/x-xwindowdump xwd -# message/cpim -# message/delivery-status -# message/disposition-notification -# message/example -# message/external-body -# message/feedback-report -# message/global -# message/global-delivery-status -# message/global-disposition-notification -# message/global-headers -# message/http -# message/imdn+xml -# message/news -# message/partial -message/rfc822 eml mime -# message/s-http -# message/sip -# message/sipfrag -# message/tracking-status -# message/vnd.si.simp -# model/example -model/iges igs iges -model/mesh msh mesh silo -model/vnd.collada+xml dae -model/vnd.dwf dwf -# model/vnd.flatland.3dml -model/vnd.gdl gdl -# model/vnd.gs-gdl -# model/vnd.gs.gdl -model/vnd.gtw gtw -# model/vnd.moml+xml -model/vnd.mts mts -# model/vnd.parasolid.transmit.binary -# model/vnd.parasolid.transmit.text -model/vnd.vtu vtu -model/vrml wrl vrml -# multipart/alternative -# multipart/appledouble -# multipart/byteranges -# multipart/digest -# multipart/encrypted -# multipart/example -# multipart/form-data -# multipart/header-set -# multipart/mixed -# multipart/parallel -# multipart/related -# multipart/report -# multipart/signed -# multipart/voice-message -# text/1d-interleaved-parityfec -text/calendar ics ifb -text/css css -text/csv csv -# text/directory -# text/dns -# text/ecmascript -# text/enriched -# text/example -# text/fwdred -text/html html htm -# text/javascript -text/n3 n3 -# text/parityfec -text/plain txt text conf def list log in -# text/prs.fallenstein.rst -text/prs.lines.tag dsc -# text/vnd.radisys.msml-basic-layout -# text/red -# text/rfc822-headers -text/richtext rtx -# text/rtf -# text/rtp-enc-aescm128 -# text/rtx -text/sgml sgml sgm -# text/t140 -text/tab-separated-values tsv -text/troff t tr roff man me ms -text/turtle ttl -# text/ulpfec -text/uri-list uri uris urls -text/vcard vcard -# text/vnd.abc -text/vnd.curl curl -text/vnd.curl.dcurl dcurl -text/vnd.curl.scurl scurl -text/vnd.curl.mcurl mcurl -# text/vnd.dmclientscript -text/vnd.dvb.subtitle sub -# text/vnd.esmertec.theme-descriptor -text/vnd.fly fly -text/vnd.fmi.flexstor flx -text/vnd.graphviz gv -text/vnd.in3d.3dml 3dml -text/vnd.in3d.spot spot -# text/vnd.iptc.newsml -# text/vnd.iptc.nitf -# text/vnd.latex-z -# text/vnd.motorola.reflex -# text/vnd.ms-mediapackage -# text/vnd.net2phone.commcenter.command -# text/vnd.si.uricatalogue -text/vnd.sun.j2me.app-descriptor jad -# text/vnd.trolltech.linguist -# text/vnd.wap.si -# text/vnd.wap.sl -text/vnd.wap.wml wml -text/vnd.wap.wmlscript wmls -text/x-asm s asm -text/x-c c cc cxx cpp h hh dic -text/x-fortran f for f77 f90 -text/x-pascal p pas -text/x-java-source java -text/x-setext etx -text/x-uuencode uu -text/x-vcalendar vcs -text/x-vcard vcf -# text/xml -# text/xml-external-parsed-entity -# video/1d-interleaved-parityfec -video/3gpp 3gp -# video/3gpp-tt -video/3gpp2 3g2 -# video/bmpeg -# video/bt656 -# video/celb -# video/dv -# video/example -video/h261 h261 -video/h263 h263 -# video/h263-1998 -# video/h263-2000 -video/h264 h264 -# video/h264-rcdo -# video/h264-svc -video/jpeg jpgv -# video/jpeg2000 -video/jpm jpm jpgm -video/mj2 mj2 mjp2 -# video/mp1s -# video/mp2p -# video/mp2t -video/mp4 mp4 mp4v mpg4 -# video/mp4v-es -video/mpeg mpeg mpg mpe m1v m2v -# video/mpeg4-generic -# video/mpv -# video/nv -video/ogg ogv -# video/parityfec -# video/pointer -video/quicktime qt mov -# video/raw -# video/rtp-enc-aescm128 -# video/rtx -# video/smpte292m -# video/ulpfec -# video/vc1 -# video/vnd.cctv -video/vnd.dece.hd uvh uvvh -video/vnd.dece.mobile uvm uvvm -# video/vnd.dece.mp4 -video/vnd.dece.pd uvp uvvp -video/vnd.dece.sd uvs uvvs -video/vnd.dece.video uvv uvvv -# video/vnd.directv.mpeg -# video/vnd.directv.mpeg-tts -# video/vnd.dlna.mpeg-tts -video/vnd.dvb.file dvb -video/vnd.fvt fvt -# video/vnd.hns.video -# video/vnd.iptvforum.1dparityfec-1010 -# video/vnd.iptvforum.1dparityfec-2005 -# video/vnd.iptvforum.2dparityfec-1010 -# video/vnd.iptvforum.2dparityfec-2005 -# video/vnd.iptvforum.ttsavc -# video/vnd.iptvforum.ttsmpeg2 -# video/vnd.motorola.video -# video/vnd.motorola.videop -video/vnd.mpegurl mxu m4u -video/vnd.ms-playready.media.pyv pyv -# video/vnd.nokia.interleaved-multimedia -# video/vnd.nokia.videovoip -# video/vnd.objectvideo -# video/vnd.sealed.mpeg1 -# video/vnd.sealed.mpeg4 -# video/vnd.sealed.swf -# video/vnd.sealedmedia.softseal.mov -video/vnd.uvvu.mp4 uvu uvvu -video/vnd.vivo viv -video/webm webm -video/x-f4v f4v -video/x-fli fli -video/x-flv flv -video/x-m4v m4v -video/x-ms-asf asf asx -video/x-ms-wm wm -video/x-ms-wmv wmv -video/x-ms-wmx wmx -video/x-ms-wvx wvx -video/x-msvideo avi -video/x-sgi-movie movie -x-conference/x-cooltalk ice +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpp-ims+xml +# application/activemessage +application/andrew-inset ez +# application/applefile +application/applixware aw +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomicmail +application/atomsvc+xml atomsvc +# application/auth-policy+xml +# application/batch-smtp +# application/beep+xml +# application/calendar+xml +# application/cals-1840 +# application/ccmp+xml +application/ccxml+xml ccxml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cnrp+xml +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csta+xml +# application/cstadata+xml +application/cu-seeme cu +# application/cybercash +application/davmount+xml davmount +# application/dca-rft +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dns +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +application/emma+xml emma +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fits +application/font-tdpfr pfr +# application/framework-attributes+xml +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-request+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +application/json json +# application/kpml-request+xml +# application/kpml-response+xml +application/lost+xml lostxml +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +# application/mathml-content+xml +# application/mathml-presentation+xml +application/mathml+xml mathml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media_control+xml +application/mediaservercontrol+xml mscml +application/metalink4+xml meta4 +application/mets+xml mets +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-request +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nss +# application/ocsp-request +# application/ocsp-response +application/octet-stream bin dms lha lrf lzh so iso dmg dist distz pkg bpk dump elc deploy +application/oda oda +application/oebps-package+xml opf +application/ogg ogx +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/tve-trigger +# application/ulpfec +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp.bsf+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +application/vnd.chemdraw+xml cdxml +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.collection+json +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cybank +application/vnd.data-vision.rdz rdz +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.dir-bi.plate-dl-nosuffix +application/vnd.dna dna +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-request+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.fints +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +# application/vnd.fujixerox.hbpl +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +application/vnd.hzn-3d-crossword x3d +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.packageitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-request+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-request +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.quobject-quoxdocument +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +# application/vnd.otps.ct-kip+xml +application/vnd.palm pdb pqa oprc +# application/vnd.paos.xml +application/vnd.pawaafile paw +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.route66.link66+xml link66 +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +# application/vnd.sun.wadl+xml +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.wsc +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cdlink vcd +application/x-chat chat +application/x-chess-pgn pgn +# application/x-compress +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-ttf ttf ttc +application/x-font-type1 pfa pfb pfm afm +application/x-font-woff woff +# application/x-font-vfont +application/x-futuresplash spl +application/x-gnumeric gnumeric +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-stuffit sit +application/x-stuffitx sitx +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-ustar ustar +application/x-wais-source src +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xpinstall xpi +# application/x400-bp +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info-diff+xml +# application/xcon-conference-info+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xmpp+xml +application/xop+xml xop +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/example +# audio/fwdred +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +audio/ogg oga ogg spx +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu-wb +# audio/pcmu +# audio/prs.sid +# audio/qcelp +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtx +# audio/smv +# audio/smv0 +# audio/smv-qcp +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file dvb +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +audio/x-wav wav +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.dvb.subtitle sub +image/vnd.djvu djvu djv +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +image/vnd.ms-modi mdi +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +image/webp webp +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# model/example +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +model/vnd.vtu vtu +model/vrml wrl vrml +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# text/1d-interleaved-parityfec +text/calendar ics ifb +text/css css +text/csv csv +# text/directory +# text/dns +# text/ecmascript +# text/enriched +# text/example +# text/fwdred +text/html html htm +# text/javascript +text/n3 n3 +# text/parityfec +text/plain txt text conf def list log in +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/vnd.radisys.msml-basic-layout +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.scurl scurl +text/vnd.curl.mcurl mcurl +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-pascal p pas +text/x-java-source java +text/x-setext etx +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raw +# video/rtp-enc-aescm128 +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-ms-asf asf asx +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +x-conference/x-cooltalk ice diff --git a/types/node.types b/lib/types/node.types similarity index 96% rename from types/node.types rename to lib/types/node.types index 2ab07a4..69a6641 100644 --- a/types/node.types +++ b/lib/types/node.types @@ -1,54 +1,54 @@ -# What: Google Chrome Extension -# Why: To allow apps to (work) be served with the right content type header. -# http://codereview.chromium.org/2830017 -# Added by: niftylettuce -application/x-chrome-extension crx - -# What: OTF Message Silencer -# Why: To silence the "Resource interpreted as font but transferred with MIME -# type font/otf" message that occurs in Google Chrome -# Added by: niftylettuce -font/opentype otf - -# What: HTC support -# Why: To properly render .htc files such as CSS3PIE -# Added by: niftylettuce -text/x-component htc - -# What: HTML5 application cache manifest -# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps -# per https://developer.mozilla.org/en/offline_resources_in_firefox -# Added by: louisremi -text/cache-manifest appcache manifest - -# What: node binary buffer format -# Why: semi-standard extension w/in the node community -# Added by: tootallnate -application/octet-stream buffer - -# What: The "protected" MP-4 formats used by iTunes. -# Why: Required for streaming music to browsers (?) -# Added by: broofa -application/mp4 m4p -audio/mp4 m4a - -# What: Music playlist format (http://en.wikipedia.org/wiki/M3U) -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -application/x-mpegURL m3u8 - -# What: Video format, Part of RFC1890 -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -video/MP2T ts - -# What: The FLAC lossless codec format -# Why: Streaming and serving FLAC audio -# Added by: jacobrask -audio/flac flac - -# What: EventSource mime type -# Why: mime type of Server-Sent Events stream -# http://www.w3.org/TR/eventsource/#text-event-stream -# Added by: francois2metz -text/event-stream event-stream +# What: Google Chrome Extension +# Why: To allow apps to (work) be served with the right content type header. +# http://codereview.chromium.org/2830017 +# Added by: niftylettuce +application/x-chrome-extension crx + +# What: OTF Message Silencer +# Why: To silence the "Resource interpreted as font but transferred with MIME +# type font/otf" message that occurs in Google Chrome +# Added by: niftylettuce +font/opentype otf + +# What: HTC support +# Why: To properly render .htc files such as CSS3PIE +# Added by: niftylettuce +text/x-component htc + +# What: HTML5 application cache manifest +# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps +# per https://developer.mozilla.org/en/offline_resources_in_firefox +# Added by: louisremi +text/cache-manifest appcache manifest + +# What: node binary buffer format +# Why: semi-standard extension w/in the node community +# Added by: tootallnate +application/octet-stream buffer + +# What: The "protected" MP-4 formats used by iTunes. +# Why: Required for streaming music to browsers (?) +# Added by: broofa +application/mp4 m4p +audio/mp4 m4a + +# What: Music playlist format (http://en.wikipedia.org/wiki/M3U) +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +application/x-mpegURL m3u8 + +# What: Video format, Part of RFC1890 +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +video/MP2T ts + +# What: The FLAC lossless codec format +# Why: Streaming and serving FLAC audio +# Added by: jacobrask +audio/flac flac + +# What: EventSource mime type +# Why: mime type of Server-Sent Events stream +# http://www.w3.org/TR/eventsource/#text-event-stream +# Added by: francois2metz +text/event-stream event-stream diff --git a/screenshot2.png b/screenshot.png similarity index 100% rename from screenshot2.png rename to screenshot.png diff --git a/screenshot1.png b/screenshot1.png deleted file mode 100644 index 7f64bef1dd6d576ac4105e853e1a84685197712e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84448 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 -1 ? lastIdx : cfgPath.lastIndexOf('/') ); + + //read a file line-by-line + contents.match(/[^\r\n]+/g).forEach(function(line){ + //ignore comments that begin with '#' + if(line[0] != '#'){ + files.push(path.join(dir, line)); + } + }); + + return files; + }, + + //Watch changes on list of files + watchFiles: function(files){ + files.forEach(function(file){ + fs.watch(file, function(evt, filename){ + combine.combine(files); + }); + }); + }, + + //Watch changes on source folder + watchDir: function(directory){ + fs.watch(directory, function(evt, filename){ + combine.combineDir(directory); + }); + }, + + //Combine directory + combineDir: function(directory){ + try{ + var allFiles = fs.readdirSync(directory), + //File name must be consist of numbers characters or "-" "_", "." + fileReg = /^[a-zA-Z0-9-_\.]+$/, + files = []; + + allFiles.forEach(function(file){ + if(fileReg.test(file)){ + files.push(path.join(directory, file)); + }else{ + console.log("Skip file:" + file); + } + }); + + return combine.combine(files); + } + catch(err){ + console.log(err); + return false; + } + }, + + //Combine set of files into one + combine: function(files){ + var oStream = combine.getStream(), r = true; + if(!oStream){ + return false; + } + + try{ + files.forEach(function(file){ + var stat = fs.statSync(file); + + if(!stat.isFile()){ + console.log("Skip folder:" + file); + }else{ + var data = fs.readFileSync(file); + oStream.write("/*" + file + "*/\r\n"); + oStream.write(data); + oStream.write("\r\n"); + + console.log("Adding file:" + file); + } + }); + oStream.end(); + + var endTime = new Date(); + console.log("count:", files.length, + ", date:", new Date().toTimeString(), + "\r\n\r\n" + ); + } + catch(err){ + console.log(err); + r = false; + } + + return r; + } + }; + + /* + * parsing parameters from command line + * etc, node combine.js -i configfile.path -o outputfile.path + * the parameter will be: '-' + one character, like: parsing('-o'); + */ + var parsing = function(args, key){ + if(!key || key.length != 2 || key[0] != '-') return; + + var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), + param = args.match(reg); + + if(param && param[0]){ + return param[0].substr(4, 500); + } + }; + + /* + * call it + * -i filepath: input directory or cfg file + * -o filepath: output files + * -w: keep watch the changes? + */ + (function(){ + var args = process.argv.join(' '), + input = parsing(args, '-i'), + output = parsing(args, '-o'); + + combine.init(input, output, args.indexOf(' -w') > 0); + + })(); + +})(); From 34b240c3649612f3b315e667dfdef4b88c867823 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 11 Jul 2012 09:59:14 +0800 Subject: [PATCH 002/195] delete debug files --- build_all.js | 304 --------------------------------------------------- 1 file changed, 304 deletions(-) delete mode 100644 build_all.js diff --git a/build_all.js b/build_all.js deleted file mode 100644 index c984d3e..0000000 --- a/build_all.js +++ /dev/null @@ -1,304 +0,0 @@ -/*ViewSvr.js*/ -/* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, -* Author: Kris Zhang -* Blog: http://c52u.so -* Required: Node.js: http://www.nodejs.org, -* mime.js: https://github.com/bentomas/node-mime -* Date: 2012-3 Draft -* 2012-4 Update: Using async and mime.js -* 2012-7 Update: Rename and reformat files -*/ -/* -* ViewSvr Namespace -*/ -var ViewSvr = (function(){ - - /* - var defaults = { - //root directory of the web - dir: "C:\\Program Files", - //listening port. - port: 8021, - //url mapping parameters. - urlMapper: null - }; - */ - - var server = function(strDir, strPort, urlMapper){ - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"), - //it self - self = this, - //Root path - dir = "C:\\Program Files", - //Listening port - port = 8021, - //How many files? - count = 0; - - var urlFormat = function(url){ - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //align to right - var date = function(date){ - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //align to left - var size = function(num){ - return num + " ".substring(0, 12 - String(num).length); - }; - - var anchor = function(txt, url){ - url = url ? url : "/"; - return '' + txt + ""; - }; - - var requestHandler = function(request, response){ - //url redirect module - if(urlMapper && urlMapper.match(request, response)){ - return; - } - - count = 0; - - var url = request.url; - - //bug can't recognized the parameter; - url = url.replace(/\?[\w_=-]+$/g, ''); - - var fullPath = path.join(dir, url), - stat; - - - try{ - stat = fs.statSync(fullPath) - }catch(err){ - response.writeHead(404, {"Content-Type": "text/html"}); - response.end("File not found!"); - return; - } - - //List all the files in a directory including the all the sub/child folders. - var listFiles = function(callback){ - - fs.readdir(fullPath, function(err, files){ - if(err){ - console.log(err); - return; - } - - for(var idx = 0, len = files.length; idx < len; idx++){ - //persitent the idx before make the sync process - (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - if(err){ - console.log(err); - return; - } - - count++; - - response.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - - idx == len -1 && callback(); - }); - })(idx); - } - }); - }; - - //Is file? Open this file and send to client. - if(stat.isFile()){ - fs.readFile(fullPath, function(err, data){ - if(err){ - console.log(err); - return; - } - response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); - response.end(data, "binary"); - }); - } - //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ - response.writeHead(200, {"Content-Type": "text/html"}); - response.write("

http://localhost:" + port + url + "


"); - response.write("
");
-        response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
-        listFiles(function(){
-          response.write("

"); - response.end("
Count: " + count + "
"); - }); - } - }; - - /* - public start http server; - */ - self.start = function(){ - // Entry Point - (function(args){ - dir = args[2] || strDir || dir; - port = Number(args[3]) || Number(strPort) || port; - - try{ - //Create http server - var httpSvr = require("http").createServer(requestHandler); - httpSvr.listen(port); - - console.log("Running at localhost" - ,"dir:", dir - ,"Port:", port - ); - - self.httpSvr = httpSvr; - - return true; - } - catch(err){ - console.log("Can't setup server at port", port, err); - } - return false; - })(process.argv); - - }; - - /* - public close http server; - */ - self.close = function(){ - if(self.httpSvr){ - self.httpSvr.close(); - return true; - } - return false; - }; - - }; - - return server; - -})(); - -/*UrlMap.js*/ -var UrlMapper = function(){ - var self = this, - maps = []; - - self.add = function(regExp, handler){ - maps.push({regExp: regExp, handler: handler}); - }; - - /* - Map the url - */ - self.match = function(req, res){ - for(var i = 0, len = maps.length; i < len ; i++){ - var mapper = maps[i]; - if(mapper.regExp && mapper.regExp.test(req.url)){ - try{ - mapper.handler && mapper.handler(req, res); - return true; - } - catch(err){ console.log(err) } - } - } - return false; - }; - -}; - -/*Main.js*/ -var urlMapper = new UrlMapper(); -var viewSvr = new ViewSvr("./../", 8000, urlMapper); -viewSvr.start(); - -/* -UrlMapper example: close server -http://localhost:8000/admin/close -*/ -urlMapper.add(/admin\/close/g, function(req, res){ - res.writeHead(200, {"Content-Type": "text/plain"}); - res.end("server is closed"); - viewSvr.close(); -}); - - - - - - - - -var fs = require("fs"); - -urlMapper.add(/xhrlogin.htm/g, function(req, res){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.end('{"cookie":123456789}'); -}); - -urlMapper.add(/xhrlogin.htm/g, function(req, res){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.end('{"cookie":123456789}'); -}); - -urlMapper.add(/xhrindex.htm/g, function(req, res){ - var data = fs.readFileSync("D:\\www\\pduweb\\Web\\index.htm", "UTF-8"); - res.writeHead(200, {"Content-Type": "text/html" }); - res.end(data, "binary"); -}); - -urlMapper.add(/xhrmasterpg.htm/g, function(req, res){ - var data = fs.readFileSync("D:\\www\\pduweb\\Web\\masterpg.htm", "UTF-8"); - res.writeHead(200, {"Content-Type": "text/html" }); - res.end(data, "binary"); -}); - -urlMapper.add(/xhrdashboard.htm/g, function(req, res){ - var data = fs.readFileSync("D:\\www\\pduweb\\Web\\dashboar.htm", "UTF-8"); - res.writeHead(200, {"Content-Type": "text/html" }); - res.end(data, "binary"); -}); - -urlMapper.add(/xhrupdtfw.htm/g, function(req, res){ - var data = fs.readFileSync("D:\\www\\pduweb\\Web\\updtfw2.htm", "UTF-8"); - res.writeHead(200, {"Content-Type": "text/html" }); - res.end(data, "binary"); -}); - -urlMapper.add(/xhrfwfilepost.htm/g, function(req, res){ - - var formidable = require('./lib/incoming_form'); - - var form = new formidable.IncomingForm(); - form.parse(req, function(err, fields, files) { - if (err){ - console.log(err); - return; - }; - - //client.putFile("upload/"+files.upload.name, files.upload.name, function(err, res){} }); - res.writeHead(200, {'content-type': 'text/plain'}); - res.end(JSON.stringify({fields: fields, files: files})); - }); - -}); From 005b2f3f588889a0e18516ff64e8b45a7455f686 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 31 Jul 2012 14:41:37 +0800 Subject: [PATCH 003/195] Add request parser and urlmapper object. 1) Add RequestParser.js 2) Add file mapping in the UrlMapper. 3) Rename to WebSvr --- Main.js | 27 ++++-- MakeFile.list | 3 +- README | 41 ++++++++- RequestParser.js | 20 +++++ UrlMapper.js | 45 ++++++++-- ViewSvr.all.js | 185 ++++++++++++++++++++++++++++++---------- ViewSvr.js => WebSvr.js | 90 ++++++++++++------- 7 files changed, 320 insertions(+), 91 deletions(-) create mode 100644 RequestParser.js rename ViewSvr.js => WebSvr.js (67%) diff --git a/Main.js b/Main.js index f6cba2a..6ba0f26 100644 --- a/Main.js +++ b/Main.js @@ -1,17 +1,34 @@ /* Main : start the server +treate the current folder as the default folder */ - var urlMapper = new UrlMapper(); -var viewSvr = new ViewSvr("./../", 8000, urlMapper); -viewSvr.start(); +var webSvr = new WebSvr("./", 8000, urlMapper); +webSvr.start(); /* UrlMapper example: close server -http://localhost:8000/admin/close +try it at: http://localhost:8000/admin/close */ urlMapper.add(/admin\/close/g, function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); res.end("server is closed"); - viewSvr.close(); + webSvr.close(); +}); + +/* +Map build.txt to tool/Combine.js +try it at: http://localhost:8000/build.txt +*/ +urlMapper.add(/build.txt/, ["tool/Combine.js"]); + +/* +Map post.htm, and write the post data on the data; +try it at: http://localhost:8000/post.htm +*/ +urlMapper.parse(/post.htm/, function(req, res, data){ + res.write('
') + res.write('') + res.write('

'); + res.end(data); }); \ No newline at end of file diff --git a/MakeFile.list b/MakeFile.list index db12131..87e17ed 100644 --- a/MakeFile.list +++ b/MakeFile.list @@ -1,6 +1,7 @@ #config file #These modules will be crunched to one. +RequestParser.js UrlMapper.js -ViewSvr.js +WebSvr.js Main.js diff --git a/README b/README index 240ba36..e08b96f 100644 --- a/README +++ b/README @@ -1,3 +1,38 @@ -Create a static file server (http based). -This will list all the files and directories via Node.Js. -The behavior will be like directory browsing enabled in IIS. \ No newline at end of file +Create a static file server (http based). +This will list all the files and directories via Node.Js. +The behavior will be like directory browsing enabled in IIS. + +/* +Main : start the server +treate the current folder as the default folder +*/ +var urlMapper = new UrlMapper(); +var webSvr = new WebSvr("./", 8000, urlMapper); +webSvr.start(); + +/* +UrlMapper example: close server +try it at: http://localhost:8000/admin/close +*/ +urlMapper.add(/admin\/close/g, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + res.end("server is closed"); + webSvr.close(); +}); + +/* +Map build.txt to tool/Combine.js +try it at: http://localhost:8000/build.txt +*/ +urlMapper.add(/build.txt/, ["tool/Combine.js"]); + +/* +Map post.htm, and write the post data on the data; +try it at: http://localhost:8000/post.htm +*/ +urlMapper.parse(/post.htm/, function(req, res, data){ + res.write('
') + res.write('') + res.write('

'); + res.end(data); +}); \ No newline at end of file diff --git a/RequestParser.js b/RequestParser.js new file mode 100644 index 0000000..0981b25 --- /dev/null +++ b/RequestParser.js @@ -0,0 +1,20 @@ +/* RequestParser.js +Request parser, +when parse complete, execute the callback, with response data; +*/ +var RequestParser = function(req, res, callback){ + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE), + length = 0, + data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(req, res, data); + }); +}; \ No newline at end of file diff --git a/UrlMapper.js b/UrlMapper.js index dbcb531..465d804 100644 --- a/UrlMapper.js +++ b/UrlMapper.js @@ -1,25 +1,56 @@ -/* -UrlMapper : mapping url; +/* UrlMap.js +Url mapper */ - var UrlMapper = function(){ var self = this, maps = []; + /* + Add a maching rule + */ self.add = function(regExp, handler){ maps.push({regExp: regExp, handler: handler}); }; + self.parse = function(regExp, handler){ + maps.push({regExp: regExp, handler: handler, parse: true}); + }; + /* - Map the url + Mapping the url */ self.match = function(req, res){ for(var i = 0, len = maps.length; i < len ; i++){ + var mapper = maps[i]; if(mapper.regExp && mapper.regExp.test(req.url)){ + try{ - mapper.handler && mapper.handler(req, res); - return true; + var handler = mapper.handler; + + switch(typeof handler){ + //function: treated it as custom function handler + case "function": + //need to parse the request? + if(mapper.parse){ + RequestParser(req, res, handler); + }else{ + handler(req, res); + } + return true; + + //string: treated it as content + case "string": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + return true; + + //array: array is an object, treated it as file. + case "object": + webSvr.tryWriteFile(res, handler[0]); + return true; + } + console.log(typeof handler, handler); } catch(err){ console.log(err) } } @@ -27,4 +58,6 @@ var UrlMapper = function(){ return false; }; + return self; + }; \ No newline at end of file diff --git a/ViewSvr.all.js b/ViewSvr.all.js index bdfe337..d807f04 100644 --- a/ViewSvr.all.js +++ b/ViewSvr.all.js @@ -1,26 +1,78 @@ +/*RequestParser.js*/ +/* RequestParser.js +Request parser, +when parse complete, execute the callback, with response data; +*/ +var RequestParser = function(req, res, callback){ + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE), + length = 0, + data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(req, res, data); + }); +}; /*UrlMapper.js*/ -/* -UrlMapper : mapping url; +/* UrlMap.js +Url mapper */ - var UrlMapper = function(){ var self = this, maps = []; + /* + Add a maching rule + */ self.add = function(regExp, handler){ maps.push({regExp: regExp, handler: handler}); }; + self.parse = function(regExp, handler){ + maps.push({regExp: regExp, handler: handler, parse: true}); + }; + /* - Map the url + Mapping the url */ self.match = function(req, res){ for(var i = 0, len = maps.length; i < len ; i++){ + var mapper = maps[i]; if(mapper.regExp && mapper.regExp.test(req.url)){ + try{ - mapper.handler && mapper.handler(req, res); - return true; + var handler = mapper.handler; + + switch(typeof handler){ + //function: treated it as custom function handler + case "function": + //need to parse the request? + if(mapper.parse){ + RequestParser(req, res, handler); + }else{ + handler(req, res); + } + return true; + + //string: treated it as content + case "string": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + return true; + + //array: array is an object, treated it as file. + case "object": + webSvr.tryWriteFile(res, handler[0]); + return true; + } + console.log(typeof handler, handler); } catch(err){ console.log(err) } } @@ -28,14 +80,17 @@ var UrlMapper = function(){ return false; }; + return self; + }; -/*ViewSvr.js*/ +/*WebSvr.js*/ +/*WebSvr.js*/ /* * Description: Create a static file server (http based). * This will list all the files and directories via Node.Js. * The behavior will be like directory browsing enabled in IIS, * Author: Kris Zhang -* Blog: http://c52u.so +* Blog: http://c52u.com * Required: Node.js: http://www.nodejs.org, * mime.js: https://github.com/bentomas/node-mime * Date: 2012-3 Draft @@ -43,9 +98,9 @@ var UrlMapper = function(){ * 2012-7 Update: Rename and reformat files */ /* -* ViewSvr Namespace +* WebSvr Namespace */ -var ViewSvr = (function(){ +var WebSvr = (function(){ /* var defaults = { @@ -104,15 +159,15 @@ var ViewSvr = (function(){ count = 0; - var url = request.url; + var url = request.url, + hasQuery = url.indexOf("?"); - //bug can't recognized the parameter; - url = url.replace(/\?[\w_=-]+$/g, ''); + //bug: path.join can't recognize the querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; var fullPath = path.join(dir, url), stat; - try{ stat = fs.statSync(fullPath) }catch(err){ @@ -121,7 +176,7 @@ var ViewSvr = (function(){ return; } - //List all the files in a directory including the all the sub/child folders. + //List all the files in a directory. var listFiles = function(callback){ fs.readdir(fullPath, function(err, files){ @@ -131,27 +186,26 @@ var ViewSvr = (function(){ } for(var idx = 0, len = files.length; idx < len; idx++){ - //persitent the idx before make the sync process + //persistent the idx before make the sync process (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); fs.stat(filePath, function(err, stat){ + count++; + if(err){ console.log(err); - return; + }else{ + response.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); } - count++; - - response.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - - idx == len -1 && callback(); + count == len && callback(); }); })(idx); } @@ -160,14 +214,7 @@ var ViewSvr = (function(){ //Is file? Open this file and send to client. if(stat.isFile()){ - fs.readFile(fullPath, function(err, data){ - if(err){ - console.log(err); - return; - } - response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); - response.end(data, "binary"); - }); + self.writeFile(response, fullPath); } //Is Directory? List all the files and folders. else if(stat.isDirectory()){ @@ -182,8 +229,41 @@ var ViewSvr = (function(){ } }; + self.writeFile = function(response, fullPath){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + response.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + response.end(data, "binary"); + }); + }; + /* - public start http server; + try write file, we don't know the path is relative or absolute. + */ + self.tryWriteFile = function(response, filePath){ + var stat; + try{ + stat = fs.statSync(filePath) + } + catch(err){ + try{ + filePath = path.join(dir, filePath); + stat = fs.statSync(filePath); + } + catch(e){ + response.writeHead(404, {"Content-Type": "text/html"}); + response.end("File not found!"); + } + } + + self.writeFile(response, filePath); + }; + + /* + public: start http server */ self.start = function(){ // Entry Point @@ -214,7 +294,7 @@ var ViewSvr = (function(){ }; /* - public close http server; + public: close http server; */ self.close = function(){ if(self.httpSvr){ @@ -232,18 +312,35 @@ var ViewSvr = (function(){ /*Main.js*/ /* Main : start the server +treate the current folder as the default folder */ - var urlMapper = new UrlMapper(); -var viewSvr = new ViewSvr("./../", 8000, urlMapper); -viewSvr.start(); +var webSvr = new WebSvr("./", 8000, urlMapper); +webSvr.start(); /* UrlMapper example: close server -http://localhost:8000/admin/close +try it at: http://localhost:8000/admin/close */ urlMapper.add(/admin\/close/g, function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); res.end("server is closed"); - viewSvr.close(); + webSvr.close(); +}); + +/* +Map build.txt to tool/Combine.js +try it at: http://localhost:8000/build.txt +*/ +urlMapper.add(/build.txt/, ["tool/Combine.js"]); + +/* +Map post.htm, and write the post data on the data; +try it at: http://localhost:8000/post.htm +*/ +urlMapper.parse(/post.htm/, function(req, res, data){ + res.write('
') + res.write('') + res.write('

'); + res.end(data); }); diff --git a/ViewSvr.js b/WebSvr.js similarity index 67% rename from ViewSvr.js rename to WebSvr.js index c90c47b..aa3674c 100644 --- a/ViewSvr.js +++ b/WebSvr.js @@ -1,9 +1,10 @@ +/*WebSvr.js*/ /* * Description: Create a static file server (http based). * This will list all the files and directories via Node.Js. * The behavior will be like directory browsing enabled in IIS, * Author: Kris Zhang -* Blog: http://c52u.so +* Blog: http://c52u.com * Required: Node.js: http://www.nodejs.org, * mime.js: https://github.com/bentomas/node-mime * Date: 2012-3 Draft @@ -11,9 +12,9 @@ * 2012-7 Update: Rename and reformat files */ /* -* ViewSvr Namespace +* WebSvr Namespace */ -var ViewSvr = (function(){ +var WebSvr = (function(){ /* var defaults = { @@ -72,15 +73,15 @@ var ViewSvr = (function(){ count = 0; - var url = request.url; + var url = request.url, + hasQuery = url.indexOf("?"); - //bug can't recognized the parameter; - url = url.replace(/\?[\w_=-]+$/g, ''); + //bug: path.join can't recognize the querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; var fullPath = path.join(dir, url), stat; - try{ stat = fs.statSync(fullPath) }catch(err){ @@ -89,7 +90,7 @@ var ViewSvr = (function(){ return; } - //List all the files in a directory including the all the sub/child folders. + //List all the files in a directory. var listFiles = function(callback){ fs.readdir(fullPath, function(err, files){ @@ -99,27 +100,26 @@ var ViewSvr = (function(){ } for(var idx = 0, len = files.length; idx < len; idx++){ - //persitent the idx before make the sync process + //persistent the idx before make the sync process (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); fs.stat(filePath, function(err, stat){ + count++; + if(err){ console.log(err); - return; + }else{ + response.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); } - count++; - - response.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - - idx == len -1 && callback(); + count == len && callback(); }); })(idx); } @@ -128,14 +128,7 @@ var ViewSvr = (function(){ //Is file? Open this file and send to client. if(stat.isFile()){ - fs.readFile(fullPath, function(err, data){ - if(err){ - console.log(err); - return; - } - response.writeHead(200, {"Content-Type": mime.lookup(fullPath) }); - response.end(data, "binary"); - }); + self.writeFile(response, fullPath); } //Is Directory? List all the files and folders. else if(stat.isDirectory()){ @@ -150,8 +143,41 @@ var ViewSvr = (function(){ } }; + self.writeFile = function(response, fullPath){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + response.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + response.end(data, "binary"); + }); + }; + + /* + try write file, we don't know the path is relative or absolute. + */ + self.tryWriteFile = function(response, filePath){ + var stat; + try{ + stat = fs.statSync(filePath) + } + catch(err){ + try{ + filePath = path.join(dir, filePath); + stat = fs.statSync(filePath); + } + catch(e){ + response.writeHead(404, {"Content-Type": "text/html"}); + response.end("File not found!"); + } + } + + self.writeFile(response, filePath); + }; + /* - public start http server; + public: start http server */ self.start = function(){ // Entry Point @@ -182,7 +208,7 @@ var ViewSvr = (function(){ }; /* - public close http server; + public: close http server; */ self.close = function(){ if(self.httpSvr){ From 94a947cf41de10db22c5f80dd1ba340c89b158c1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 31 Jul 2012 15:23:07 +0800 Subject: [PATCH 004/195] rename build --- ViewSvr.all.js => WebSvr.all.js | 0 build.bat | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename ViewSvr.all.js => WebSvr.all.js (100%) diff --git a/ViewSvr.all.js b/WebSvr.all.js similarity index 100% rename from ViewSvr.all.js rename to WebSvr.all.js diff --git a/build.bat b/build.bat index 3bc4526..4754c1d 100644 --- a/build.bat +++ b/build.bat @@ -1,6 +1,6 @@ REM Combine first -node tool/Combine.js -i makefile.list -o ViewSvr.all.js +node tool/Combine.js -i makefile.list -o WebSvr.all.js REM Then excute it -node ViewSvr.all.js +node WebSvr.all.js REM Complete Gooldbye. pause; \ No newline at end of file From 6cddfb8f81be51924d90066f47197e8c31772e8d Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 14 Sep 2012 14:38:55 +0800 Subject: [PATCH 005/195] Update and Rename * Update Combine.js to latest * Rename some APIs. --- Main.js | 26 +-- README | 29 ++-- RequestParser.js | 2 +- UrlMapper.js | 31 ++-- WebSvr.all.js | 101 ++++++------ WebSvr.js | 43 ++--- tool/Combine.js | 413 ++++++++++++++++++++++++++--------------------- 7 files changed, 353 insertions(+), 292 deletions(-) diff --git a/Main.js b/Main.js index 6ba0f26..b4d6166 100644 --- a/Main.js +++ b/Main.js @@ -1,34 +1,34 @@ + /* -Main : start the server -treate the current folder as the default folder +Main.js +Listenening on parent folder */ -var urlMapper = new UrlMapper(); -var webSvr = new WebSvr("./", 8000, urlMapper); +var webSvr = new WebSvr("./../", 8054); webSvr.start(); /* UrlMapper example: close server -try it at: http://localhost:8000/admin/close +http://localhost:8054/admin/close */ -urlMapper.add(/admin\/close/g, function(req, res){ +webSvr.url(/admin\/close/g, function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); res.end("server is closed"); - webSvr.close(); + webSVr.close(); }); /* Map build.txt to tool/Combine.js -try it at: http://localhost:8000/build.txt +try it at: http://localhost:8054/build.txt */ -urlMapper.add(/build.txt/, ["tool/Combine.js"]); +webSvr.url(/build.txt/, ["node-websvr/tool/Combine.js"]); /* -Map post.htm, and write the post data on the data; -try it at: http://localhost:8000/post.htm +Map post.htm, and parse the post data on the request; +try it at: http://localhost:8054/post.htm */ -urlMapper.parse(/post.htm/, function(req, res, data){ +webSvr.post(/post.htm/, function(req, res, data){ res.write('
') res.write('') res.write('

'); res.end(data); -}); \ No newline at end of file +}); diff --git a/README b/README index e08b96f..521d80f 100644 --- a/README +++ b/README @@ -1,38 +1,35 @@ -Create a static file server (http based). -This will list all the files and directories via Node.Js. -The behavior will be like directory browsing enabled in IIS. +Create a web server. /* -Main : start the server -treate the current folder as the default folder +Main.js +Listenening on parent folder */ -var urlMapper = new UrlMapper(); -var webSvr = new WebSvr("./", 8000, urlMapper); +var webSvr = new WebSvr("./../", 8054); webSvr.start(); /* UrlMapper example: close server -try it at: http://localhost:8000/admin/close +http://localhost:8054/admin/close */ -urlMapper.add(/admin\/close/g, function(req, res){ +webSvr.url(/admin\/close/g, function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); res.end("server is closed"); - webSvr.close(); + webSVr.close(); }); /* Map build.txt to tool/Combine.js -try it at: http://localhost:8000/build.txt +try it at: http://localhost:8054/build.txt */ -urlMapper.add(/build.txt/, ["tool/Combine.js"]); +webSvr.url(/build.txt/, ["node-websvr/tool/Combine.js"]); /* -Map post.htm, and write the post data on the data; -try it at: http://localhost:8000/post.htm +Map post.htm, and parse the post data on the request; +try it at: http://localhost:8054/post.htm */ -urlMapper.parse(/post.htm/, function(req, res, data){ +webSvr.post(/post.htm/, function(req, res, data){ res.write('
') res.write('') res.write('

'); res.end(data); -}); \ No newline at end of file +}); diff --git a/RequestParser.js b/RequestParser.js index 0981b25..dd76947 100644 --- a/RequestParser.js +++ b/RequestParser.js @@ -1,4 +1,4 @@ -/* RequestParser.js +/* Request parser, when parse complete, execute the callback, with response data; */ diff --git a/UrlMapper.js b/UrlMapper.js index 465d804..1f14fc1 100644 --- a/UrlMapper.js +++ b/UrlMapper.js @@ -1,19 +1,22 @@ -/* UrlMap.js +/* Url mapper */ -var UrlMapper = function(){ +var UrlMapper = function(webSvr){ var self = this, maps = []; /* - Add a maching rule + Handle matched rule; */ - self.add = function(regExp, handler){ - maps.push({regExp: regExp, handler: handler}); + self.url = function(regExp, handler, parse){ + maps.push({regExp: regExp, handler: handler, parse: parse}); }; - self.parse = function(regExp, handler){ - maps.push({regExp: regExp, handler: handler, parse: true}); + /* + Parse the post request data + */ + self.post = function(regExp, handler){ + self.url(regExp, handler, true); }; /* @@ -26,11 +29,12 @@ var UrlMapper = function(){ if(mapper.regExp && mapper.regExp.test(req.url)){ try{ - var handler = mapper.handler; + var handler = mapper.handler, + type = handler.constructor.name; - switch(typeof handler){ + switch(type){ //function: treated it as custom function handler - case "function": + case "Function": //need to parse the request? if(mapper.parse){ RequestParser(req, res, handler); @@ -40,17 +44,16 @@ var UrlMapper = function(){ return true; //string: treated it as content - case "string": + case "String": res.writeHead(200, { "Content-Type": "text/html" }); res.end(handler); return true; - //array: array is an object, treated it as file. - case "object": + //array: treated it as a file. + case "Array": webSvr.tryWriteFile(res, handler[0]); return true; } - console.log(typeof handler, handler); } catch(err){ console.log(err) } } diff --git a/WebSvr.all.js b/WebSvr.all.js index d807f04..084b385 100644 --- a/WebSvr.all.js +++ b/WebSvr.all.js @@ -1,5 +1,5 @@ /*RequestParser.js*/ -/* RequestParser.js +/* Request parser, when parse complete, execute the callback, with response data; */ @@ -20,22 +20,25 @@ var RequestParser = function(req, res, callback){ }); }; /*UrlMapper.js*/ -/* UrlMap.js +/* Url mapper */ -var UrlMapper = function(){ +var UrlMapper = function(webSvr){ var self = this, maps = []; /* - Add a maching rule + Handle matched rule; */ - self.add = function(regExp, handler){ - maps.push({regExp: regExp, handler: handler}); + self.url = function(regExp, handler, parse){ + maps.push({regExp: regExp, handler: handler, parse: parse}); }; - self.parse = function(regExp, handler){ - maps.push({regExp: regExp, handler: handler, parse: true}); + /* + Parse the post request data + */ + self.post = function(regExp, handler){ + self.url(regExp, handler, true); }; /* @@ -48,11 +51,12 @@ var UrlMapper = function(){ if(mapper.regExp && mapper.regExp.test(req.url)){ try{ - var handler = mapper.handler; + var handler = mapper.handler, + type = handler.constructor.name; - switch(typeof handler){ + switch(type){ //function: treated it as custom function handler - case "function": + case "Function": //need to parse the request? if(mapper.parse){ RequestParser(req, res, handler); @@ -62,17 +66,16 @@ var UrlMapper = function(){ return true; //string: treated it as content - case "string": + case "String": res.writeHead(200, { "Content-Type": "text/html" }); res.end(handler); return true; - //array: array is an object, treated it as file. - case "object": + //array: treated it as a file. + case "Array": webSvr.tryWriteFile(res, handler[0]); return true; } - console.log(typeof handler, handler); } catch(err){ console.log(err) } } @@ -84,7 +87,6 @@ var UrlMapper = function(){ }; /*WebSvr.js*/ -/*WebSvr.js*/ /* * Description: Create a static file server (http based). * This will list all the files and directories via Node.Js. @@ -102,23 +104,18 @@ var UrlMapper = function(){ */ var WebSvr = (function(){ - /* - var defaults = { - //root directory of the web - dir: "C:\\Program Files", - //listening port. - port: 8021, - //url mapping parameters. - urlMapper: null - }; - */ - - var server = function(strDir, strPort, urlMapper){ - var fs = require("fs"), + var server = function(strDir, strPort){ + /* + Library + */ + var fs = require("fs"), path = require("path"), - mime = require("./lib/mime"), - //it self - self = this, + mime = require("./lib/mime"); + + /* + Parameters + */ + var self = this, //Root path dir = "C:\\Program Files", //Listening port @@ -126,6 +123,13 @@ var WebSvr = (function(){ //How many files? count = 0; + /* + Modules + */ + var urlMapper = new UrlMapper(self); + + + var urlFormat = function(url){ url = url.replace(/\\/g,'/'); url = url.replace(/ /g,'%20'); @@ -152,8 +156,8 @@ var WebSvr = (function(){ }; var requestHandler = function(request, response){ - //url redirect module - if(urlMapper && urlMapper.match(request, response)){ + //If request matched, send the request; + if(urlMapper.match(request, response)){ return; } @@ -229,6 +233,10 @@ var WebSvr = (function(){ } }; + /* Expose the urlMapper handle method*/ + self.url = urlMapper.url; + self.post = urlMapper.post; + self.writeFile = function(response, fullPath){ fs.readFile(fullPath, function(err, data){ if(err){ @@ -310,37 +318,38 @@ var WebSvr = (function(){ })(); /*Main.js*/ + /* -Main : start the server -treate the current folder as the default folder +Main.js +Listenening on parent folder */ -var urlMapper = new UrlMapper(); -var webSvr = new WebSvr("./", 8000, urlMapper); +var webSvr = new WebSvr("./../", 8054); webSvr.start(); /* UrlMapper example: close server -try it at: http://localhost:8000/admin/close +http://localhost:8054/admin/close */ -urlMapper.add(/admin\/close/g, function(req, res){ +webSvr.url(/admin\/close/g, function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); res.end("server is closed"); - webSvr.close(); + webSVr.close(); }); /* Map build.txt to tool/Combine.js -try it at: http://localhost:8000/build.txt +try it at: http://localhost:8054/build.txt */ -urlMapper.add(/build.txt/, ["tool/Combine.js"]); +webSvr.url(/build.txt/, ["node-websvr/tool/Combine.js"]); /* -Map post.htm, and write the post data on the data; -try it at: http://localhost:8000/post.htm +Map post.htm, and parse the post data on the request; +try it at: http://localhost:8054/post.htm */ -urlMapper.parse(/post.htm/, function(req, res, data){ +webSvr.post(/post.htm/, function(req, res, data){ res.write('
') res.write('') res.write('

'); res.end(data); }); + diff --git a/WebSvr.js b/WebSvr.js index aa3674c..8437e03 100644 --- a/WebSvr.js +++ b/WebSvr.js @@ -1,4 +1,3 @@ -/*WebSvr.js*/ /* * Description: Create a static file server (http based). * This will list all the files and directories via Node.Js. @@ -16,23 +15,18 @@ */ var WebSvr = (function(){ - /* - var defaults = { - //root directory of the web - dir: "C:\\Program Files", - //listening port. - port: 8021, - //url mapping parameters. - urlMapper: null - }; - */ - - var server = function(strDir, strPort, urlMapper){ - var fs = require("fs"), + var server = function(strDir, strPort){ + /* + Library + */ + var fs = require("fs"), path = require("path"), - mime = require("./lib/mime"), - //it self - self = this, + mime = require("./lib/mime"); + + /* + Parameters + */ + var self = this, //Root path dir = "C:\\Program Files", //Listening port @@ -40,6 +34,13 @@ var WebSvr = (function(){ //How many files? count = 0; + /* + Modules + */ + var urlMapper = new UrlMapper(self); + + + var urlFormat = function(url){ url = url.replace(/\\/g,'/'); url = url.replace(/ /g,'%20'); @@ -66,8 +67,8 @@ var WebSvr = (function(){ }; var requestHandler = function(request, response){ - //url redirect module - if(urlMapper && urlMapper.match(request, response)){ + //If request matched, send the request; + if(urlMapper.match(request, response)){ return; } @@ -143,6 +144,10 @@ var WebSvr = (function(){ } }; + /* Expose the urlMapper handle method*/ + self.url = urlMapper.url; + self.post = urlMapper.post; + self.writeFile = function(response, fullPath){ fs.readFile(fullPath, function(err, data){ if(err){ diff --git a/tool/Combine.js b/tool/Combine.js index eb8387c..b3e052a 100644 --- a/tool/Combine.js +++ b/tool/Combine.js @@ -1,183 +1,230 @@ -/* -* Description: Combine the files into one, support directory and config files. -* Author: Kris Zhang -* Blog: http://c52u.com -*/ - -(function(){ - - var fs = require("fs"), - path = require("path"); - - var combine = module.exports = { - //Combined to which file? - targetFile: "", - - //Interface - init: function(sourceFile, targetFile, watch){ - //Combine type, it's a directory or cfg file - fs.stat(sourceFile, function(err, stat){ - if(err) { - console.log(err); - return; - } - - combine.targetFile = targetFile; - - if(stat.isFile()){ - //get file list from the configuration files. - var files = combine.getFiles(sourceFile); - - if(combine.combine(files)){ - watch && combine.watchFiles(files); - } - }else{ - //Combine at the first running, then watching the changes. - if(combine.combineDir(sourceFile)){ - watch && combine.watchDir(sourceFile); - } - } - }); - }, - - //get output stream - getStream: function(){ - var stream; - try{ - stream = fs.createWriteStream(combine.targetFile); - } - catch(err){ - console.log("Can't create output stream: ", err); - } - return stream; - }, - - //get files from cfgFile, return absolute file path - getFiles: function(cfgPath){ - var contents = fs.readFileSync(cfgPath, 'utf-8'), - files = [], - lastIdx = cfgPath.lastIndexOf('\\'), - dir = cfgPath.substring(0, lastIdx > -1 ? lastIdx : cfgPath.lastIndexOf('/') ); - - //read a file line-by-line - contents.match(/[^\r\n]+/g).forEach(function(line){ - //ignore comments that begin with '#' - if(line[0] != '#'){ - files.push(path.join(dir, line)); - } - }); - - return files; - }, - - //Watch changes on list of files - watchFiles: function(files){ - files.forEach(function(file){ - fs.watch(file, function(evt, filename){ - combine.combine(files); - }); - }); - }, - - //Watch changes on source folder - watchDir: function(directory){ - fs.watch(directory, function(evt, filename){ - combine.combineDir(directory); - }); - }, - - //Combine directory - combineDir: function(directory){ - try{ - var allFiles = fs.readdirSync(directory), - //File name must be consist of numbers characters or "-" "_", "." - fileReg = /^[a-zA-Z0-9-_\.]+$/, - files = []; - - allFiles.forEach(function(file){ - if(fileReg.test(file)){ - files.push(path.join(directory, file)); - }else{ - console.log("Skip file:" + file); - } - }); - - return combine.combine(files); - } - catch(err){ - console.log(err); - return false; - } - }, - - //Combine set of files into one - combine: function(files){ - var oStream = combine.getStream(), r = true; - if(!oStream){ - return false; - } - - try{ - files.forEach(function(file){ - var stat = fs.statSync(file); - - if(!stat.isFile()){ - console.log("Skip folder:" + file); - }else{ - var data = fs.readFileSync(file); - oStream.write("/*" + file + "*/\r\n"); - oStream.write(data); - oStream.write("\r\n"); - - console.log("Adding file:" + file); - } - }); - oStream.end(); - - var endTime = new Date(); - console.log("count:", files.length, - ", date:", new Date().toTimeString(), - "\r\n\r\n" - ); - } - catch(err){ - console.log(err); - r = false; - } - - return r; - } - }; - - /* - * parsing parameters from command line - * etc, node combine.js -i configfile.path -o outputfile.path - * the parameter will be: '-' + one character, like: parsing('-o'); - */ - var parsing = function(args, key){ - if(!key || key.length != 2 || key[0] != '-') return; - - var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), - param = args.match(reg); - - if(param && param[0]){ - return param[0].substr(4, 500); - } - }; - - /* - * call it - * -i filepath: input directory or cfg file - * -o filepath: output files - * -w: keep watch the changes? - */ - (function(){ - var args = process.argv.join(' '), - input = parsing(args, '-i'), - output = parsing(args, '-o'); - - combine.init(input, output, args.indexOf(' -w') > 0); - - })(); - -})(); +/* +* Description: Combine the files into one, support directory and config files. +* Author: Kris Zhang +* Blog: http://c52u.com +*/ + +//Combine namespace +var Combine; + +(function(){ + + var fs = require("fs"), + path = require("path"), + timerID = null; + + var combine = Combine = module.exports = { + sourceFile: "", + targetFile: "", + files: [], //combine list + list: [], //watch list + watch: false, //listen on the changes? + + //Interface + init: function(sourceFile, targetFile, watch){ + //Combine type, it's a directory or cfg file + fs.stat(sourceFile, function(err, stat){ + if(err) { + console.log(err); + return; + } + + combine.sourceFile = sourceFile; + combine.targetFile = targetFile; + combine.watch = watch; + + //It's a configuration files or dictionary + if(stat.isFile()){ + combine.setCfg(sourceFile); + }else{ + combine.setDir(sourceFile); + } + }); + }, + + //get output stream + getStream: function(){ + var stream; + try{ + stream = fs.createWriteStream(combine.targetFile); + } + catch(err){ + console.log("Can't create output stream: ", err); + } + return stream; + }, + + //get files from cfgFile, return absolute file path + getFiles: function(cfgPath){ + var contents = fs.readFileSync(cfgPath, 'utf-8'), + files = [], + lastIdx = cfgPath.lastIndexOf('\\'), + dir = cfgPath.substring(0, lastIdx > -1 ? lastIdx : cfgPath.lastIndexOf('/') ); + + //read a file line-by-line + contents.match(/[^\r\n]+/g).forEach(function(line){ + //ignore comments that begin with '#' + if(line[0] != '#'){ + files.push(path.join(dir, line)); + } + }); + + combine.watch && files.forEach(function(file){ + if(combine.list.indexOf(file) < 0){ + combine.watchFile(file); + combine.list.push(file); + }; + }); + + combine.files = files; + + return files; + }, + + //Watch changes on source folder + setDir: function(directory){ + //Combine at the first running, then watching the changes. + if(combine.combineDir(directory)){ + combine.watch && fs.watch(directory, function(){ + combine.combineDir(directory); + }); + } + }, + + //Watch chagnes on configuration fiel + setCfg: function(configuration){ + var combineCfg = function(){ + //get file list from the configuration files. + combine.getFiles(configuration); + combine.combine(); + }; + + //Listen on the change on the configuration file + combine.watch && fs.watch(configuration, combineCfg); + + //combine at the first running + combineCfg(); + }, + + //Watch changes on a file + watchFile: function(file){ + try{ + fs.watch(file, function(){ + combine.combine(); + }); + } + catch(err){ + console.log(file, err); + } + }, + + //Combine directory + combineDir: function(directory){ + try{ + var allFiles = fs.readdirSync(directory), + //File name must be consist of numbers characters or "-" "_", "." + fileReg = /^[a-zA-Z0-9-_\.]+$/, + files = []; + + allFiles.forEach(function(file){ + if(fileReg.test(file)){ + files.push(path.join(directory, file)); + }else{ + console.log("Skip file:" + file); + } + }); + + combine.files = files; + + return combine.combine(); + } + catch(err){ + console.log(err); + return false; + } + }, + + //Combine set of files into one + combine: function(){ + + //Prevent other request within 1 seconds; + if(timerID) return; + + timerID = setTimeout(function(){ + timerID = null; + }, 1000); + + var oStream = combine.getStream(), r = true; + if(!oStream){ + return false; + } + + var files = combine.files; + + try{ + files.forEach(function(file){ + var stat = fs.statSync(file); + + if(!stat.isFile()){ + console.log("Skip folder:" + file); + }else{ + var data = fs.readFileSync(file); + oStream.write("/*" + file + "*/\r\n"); + oStream.write(data); + oStream.write("\r\n"); + + console.log("Adding file:" + file); + } + }); + oStream.end(); + + var endTime = new Date(); + console.log("count:", + files.length, + ", date:", new Date().toTimeString(), + "\r\n\r\n" + ); + } + catch(err){ + console.log(err); + r = false; + } + + return r; + } + }; + +})(); + + +/* +* call it from command lines +* -i filepath: input directory or cfg file +* -o filepath: output files +* -w: keep watch the changes? +*/ +(function(){ + + /* + * parsing parameters from command line + * etc, node combine.js -i configfile.path -o outputfile.path + * the parameter will be: '-' + one character, like: parsing('-o'); + */ + var parsing = function(args, key){ + if(!key || key.length != 2 || key[0] != '-') return; + + var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), + param = args.match(reg); + + if(param && param[0]){ + return param[0].substr(4, 500); + } + }; + + var args = process.argv.join(' '), + input = parsing(args, '-i'), + output = parsing(args, '-o'); + + Combine.init(input, output, args.indexOf(' -w') > 0); + +})(); From c93a5a565b911cf5e8dcefe48c405551a6e1bc54 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 23 Oct 2012 09:44:09 +0800 Subject: [PATCH 006/195] version: 0.020 - Filter: A Request will mapping all the filters first, and then pass to the Handler; - Handler: When a request matched a handler, it will returned, so only one handler will be matched; - Session: Stored in file, with JSON format; - Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ --- Main.js | 34 -- MakeFile.list | 7 - README | 35 -- RequestParser.js | 20 - UrlMapper.js | 66 --- WebSvr.all.js | 355 -------------- WebSvr.js | 230 --------- screenshot.png | Bin 916624 -> 0 bytes svr/Filter.js | 62 +++ svr/Handler.js | 86 ++++ svr/MakeFile.list | 18 + svr/Parser.js | 68 +++ svr/README.md | 94 ++++ svr/RequestParser.js | 27 ++ svr/SessionParser.js | 89 ++++ svr/SiteTest.js | 77 +++ svr/WebSvr.all.js | 780 ++++++++++++++++++++++++++++++ svr/WebSvr.js | 257 ++++++++++ build.bat => svr/build.bat | 7 +- svr/lib/file.js | 73 +++ svr/lib/incoming_form.js | 390 +++++++++++++++ {lib => svr/lib}/mime.js | 0 svr/lib/multipart_parser.js | 312 ++++++++++++ svr/lib/querystring_parser.js | 25 + {lib => svr/lib}/types/mime.types | 0 {lib => svr/lib}/types/node.types | 0 svr/lib/util.js | 6 + svr/ref/Math.uuid.js | 92 ++++ svr/ref/Object.extend.js | 10 + {tool => svr/tool}/Combine.js | 0 test/index.htm | 31 ++ test/setting.htm | 17 + 32 files changed, 2520 insertions(+), 748 deletions(-) delete mode 100644 Main.js delete mode 100644 MakeFile.list delete mode 100644 README delete mode 100644 RequestParser.js delete mode 100644 UrlMapper.js delete mode 100644 WebSvr.all.js delete mode 100644 WebSvr.js delete mode 100644 screenshot.png create mode 100644 svr/Filter.js create mode 100644 svr/Handler.js create mode 100644 svr/MakeFile.list create mode 100644 svr/Parser.js create mode 100644 svr/README.md create mode 100644 svr/RequestParser.js create mode 100644 svr/SessionParser.js create mode 100644 svr/SiteTest.js create mode 100644 svr/WebSvr.all.js create mode 100644 svr/WebSvr.js rename build.bat => svr/build.bat (59%) create mode 100644 svr/lib/file.js create mode 100644 svr/lib/incoming_form.js rename {lib => svr/lib}/mime.js (100%) create mode 100644 svr/lib/multipart_parser.js create mode 100644 svr/lib/querystring_parser.js rename {lib => svr/lib}/types/mime.types (100%) rename {lib => svr/lib}/types/node.types (100%) create mode 100644 svr/lib/util.js create mode 100644 svr/ref/Math.uuid.js create mode 100644 svr/ref/Object.extend.js rename {tool => svr/tool}/Combine.js (100%) create mode 100644 test/index.htm create mode 100644 test/setting.htm diff --git a/Main.js b/Main.js deleted file mode 100644 index b4d6166..0000000 --- a/Main.js +++ /dev/null @@ -1,34 +0,0 @@ - -/* -Main.js -Listenening on parent folder -*/ -var webSvr = new WebSvr("./../", 8054); -webSvr.start(); - -/* -UrlMapper example: close server -http://localhost:8054/admin/close -*/ -webSvr.url(/admin\/close/g, function(req, res){ - res.writeHead(200, {"Content-Type": "text/plain"}); - res.end("server is closed"); - webSVr.close(); -}); - -/* -Map build.txt to tool/Combine.js -try it at: http://localhost:8054/build.txt -*/ -webSvr.url(/build.txt/, ["node-websvr/tool/Combine.js"]); - -/* -Map post.htm, and parse the post data on the request; -try it at: http://localhost:8054/post.htm -*/ -webSvr.post(/post.htm/, function(req, res, data){ - res.write('
') - res.write('') - res.write('

'); - res.end(data); -}); diff --git a/MakeFile.list b/MakeFile.list deleted file mode 100644 index 87e17ed..0000000 --- a/MakeFile.list +++ /dev/null @@ -1,7 +0,0 @@ -#config file -#These modules will be crunched to one. - -RequestParser.js -UrlMapper.js -WebSvr.js -Main.js diff --git a/README b/README deleted file mode 100644 index 521d80f..0000000 --- a/README +++ /dev/null @@ -1,35 +0,0 @@ -Create a web server. - -/* -Main.js -Listenening on parent folder -*/ -var webSvr = new WebSvr("./../", 8054); -webSvr.start(); - -/* -UrlMapper example: close server -http://localhost:8054/admin/close -*/ -webSvr.url(/admin\/close/g, function(req, res){ - res.writeHead(200, {"Content-Type": "text/plain"}); - res.end("server is closed"); - webSVr.close(); -}); - -/* -Map build.txt to tool/Combine.js -try it at: http://localhost:8054/build.txt -*/ -webSvr.url(/build.txt/, ["node-websvr/tool/Combine.js"]); - -/* -Map post.htm, and parse the post data on the request; -try it at: http://localhost:8054/post.htm -*/ -webSvr.post(/post.htm/, function(req, res, data){ - res.write('
') - res.write('') - res.write('

'); - res.end(data); -}); diff --git a/RequestParser.js b/RequestParser.js deleted file mode 100644 index dd76947..0000000 --- a/RequestParser.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -Request parser, -when parse complete, execute the callback, with response data; -*/ -var RequestParser = function(req, res, callback){ - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE), - length = 0, - data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(req, res, data); - }); -}; \ No newline at end of file diff --git a/UrlMapper.js b/UrlMapper.js deleted file mode 100644 index 1f14fc1..0000000 --- a/UrlMapper.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -Url mapper -*/ -var UrlMapper = function(webSvr){ - var self = this, - maps = []; - - /* - Handle matched rule; - */ - self.url = function(regExp, handler, parse){ - maps.push({regExp: regExp, handler: handler, parse: parse}); - }; - - /* - Parse the post request data - */ - self.post = function(regExp, handler){ - self.url(regExp, handler, true); - }; - - /* - Mapping the url - */ - self.match = function(req, res){ - for(var i = 0, len = maps.length; i < len ; i++){ - - var mapper = maps[i]; - if(mapper.regExp && mapper.regExp.test(req.url)){ - - try{ - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type){ - //function: treated it as custom function handler - case "Function": - //need to parse the request? - if(mapper.parse){ - RequestParser(req, res, handler); - }else{ - handler(req, res); - } - return true; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - return true; - - //array: treated it as a file. - case "Array": - webSvr.tryWriteFile(res, handler[0]); - return true; - } - } - catch(err){ console.log(err) } - } - } - return false; - }; - - return self; - -}; \ No newline at end of file diff --git a/WebSvr.all.js b/WebSvr.all.js deleted file mode 100644 index 084b385..0000000 --- a/WebSvr.all.js +++ /dev/null @@ -1,355 +0,0 @@ -/*RequestParser.js*/ -/* -Request parser, -when parse complete, execute the callback, with response data; -*/ -var RequestParser = function(req, res, callback){ - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE), - length = 0, - data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(req, res, data); - }); -}; -/*UrlMapper.js*/ -/* -Url mapper -*/ -var UrlMapper = function(webSvr){ - var self = this, - maps = []; - - /* - Handle matched rule; - */ - self.url = function(regExp, handler, parse){ - maps.push({regExp: regExp, handler: handler, parse: parse}); - }; - - /* - Parse the post request data - */ - self.post = function(regExp, handler){ - self.url(regExp, handler, true); - }; - - /* - Mapping the url - */ - self.match = function(req, res){ - for(var i = 0, len = maps.length; i < len ; i++){ - - var mapper = maps[i]; - if(mapper.regExp && mapper.regExp.test(req.url)){ - - try{ - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type){ - //function: treated it as custom function handler - case "Function": - //need to parse the request? - if(mapper.parse){ - RequestParser(req, res, handler); - }else{ - handler(req, res); - } - return true; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - return true; - - //array: treated it as a file. - case "Array": - webSvr.tryWriteFile(res, handler[0]); - return true; - } - } - catch(err){ console.log(err) } - } - } - return false; - }; - - return self; - -}; -/*WebSvr.js*/ -/* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, -* Author: Kris Zhang -* Blog: http://c52u.com -* Required: Node.js: http://www.nodejs.org, -* mime.js: https://github.com/bentomas/node-mime -* Date: 2012-3 Draft -* 2012-4 Update: Using async and mime.js -* 2012-7 Update: Rename and reformat files -*/ -/* -* WebSvr Namespace -*/ -var WebSvr = (function(){ - - var server = function(strDir, strPort){ - /* - Library - */ - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); - - /* - Parameters - */ - var self = this, - //Root path - dir = "C:\\Program Files", - //Listening port - port = 8021, - //How many files? - count = 0; - - /* - Modules - */ - var urlMapper = new UrlMapper(self); - - - - var urlFormat = function(url){ - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //align to right - var date = function(date){ - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //align to left - var size = function(num){ - return num + " ".substring(0, 12 - String(num).length); - }; - - var anchor = function(txt, url){ - url = url ? url : "/"; - return '' + txt + ""; - }; - - var requestHandler = function(request, response){ - //If request matched, send the request; - if(urlMapper.match(request, response)){ - return; - } - - count = 0; - - var url = request.url, - hasQuery = url.indexOf("?"); - - //bug: path.join can't recognize the querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(dir, url), - stat; - - try{ - stat = fs.statSync(fullPath) - }catch(err){ - response.writeHead(404, {"Content-Type": "text/html"}); - response.end("File not found!"); - return; - } - - //List all the files in a directory. - var listFiles = function(callback){ - - fs.readdir(fullPath, function(err, files){ - if(err){ - console.log(err); - return; - } - - for(var idx = 0, len = files.length; idx < len; idx++){ - //persistent the idx before make the sync process - (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - count++; - - if(err){ - console.log(err); - }else{ - response.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - count == len && callback(); - }); - })(idx); - } - }); - }; - - //Is file? Open this file and send to client. - if(stat.isFile()){ - self.writeFile(response, fullPath); - } - //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ - response.writeHead(200, {"Content-Type": "text/html"}); - response.write("

http://localhost:" + port + url + "


"); - response.write("
");
-        response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
-        listFiles(function(){
-          response.write("

"); - response.end("
Count: " + count + "
"); - }); - } - }; - - /* Expose the urlMapper handle method*/ - self.url = urlMapper.url; - self.post = urlMapper.post; - - self.writeFile = function(response, fullPath){ - fs.readFile(fullPath, function(err, data){ - if(err){ - console.log(err); - return; - } - response.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); - response.end(data, "binary"); - }); - }; - - /* - try write file, we don't know the path is relative or absolute. - */ - self.tryWriteFile = function(response, filePath){ - var stat; - try{ - stat = fs.statSync(filePath) - } - catch(err){ - try{ - filePath = path.join(dir, filePath); - stat = fs.statSync(filePath); - } - catch(e){ - response.writeHead(404, {"Content-Type": "text/html"}); - response.end("File not found!"); - } - } - - self.writeFile(response, filePath); - }; - - /* - public: start http server - */ - self.start = function(){ - // Entry Point - (function(args){ - dir = args[2] || strDir || dir; - port = Number(args[3]) || Number(strPort) || port; - - try{ - //Create http server - var httpSvr = require("http").createServer(requestHandler); - httpSvr.listen(port); - - console.log("Running at localhost" - ,"dir:", dir - ,"Port:", port - ); - - self.httpSvr = httpSvr; - - return true; - } - catch(err){ - console.log("Can't setup server at port", port, err); - } - return false; - })(process.argv); - - }; - - /* - public: close http server; - */ - self.close = function(){ - if(self.httpSvr){ - self.httpSvr.close(); - return true; - } - return false; - }; - - }; - - return server; - -})(); -/*Main.js*/ - -/* -Main.js -Listenening on parent folder -*/ -var webSvr = new WebSvr("./../", 8054); -webSvr.start(); - -/* -UrlMapper example: close server -http://localhost:8054/admin/close -*/ -webSvr.url(/admin\/close/g, function(req, res){ - res.writeHead(200, {"Content-Type": "text/plain"}); - res.end("server is closed"); - webSVr.close(); -}); - -/* -Map build.txt to tool/Combine.js -try it at: http://localhost:8054/build.txt -*/ -webSvr.url(/build.txt/, ["node-websvr/tool/Combine.js"]); - -/* -Map post.htm, and parse the post data on the request; -try it at: http://localhost:8054/post.htm -*/ -webSvr.post(/post.htm/, function(req, res, data){ - res.write('
') - res.write('') - res.write('

'); - res.end(data); -}); - diff --git a/WebSvr.js b/WebSvr.js deleted file mode 100644 index 8437e03..0000000 --- a/WebSvr.js +++ /dev/null @@ -1,230 +0,0 @@ -/* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, -* Author: Kris Zhang -* Blog: http://c52u.com -* Required: Node.js: http://www.nodejs.org, -* mime.js: https://github.com/bentomas/node-mime -* Date: 2012-3 Draft -* 2012-4 Update: Using async and mime.js -* 2012-7 Update: Rename and reformat files -*/ -/* -* WebSvr Namespace -*/ -var WebSvr = (function(){ - - var server = function(strDir, strPort){ - /* - Library - */ - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); - - /* - Parameters - */ - var self = this, - //Root path - dir = "C:\\Program Files", - //Listening port - port = 8021, - //How many files? - count = 0; - - /* - Modules - */ - var urlMapper = new UrlMapper(self); - - - - var urlFormat = function(url){ - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //align to right - var date = function(date){ - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //align to left - var size = function(num){ - return num + " ".substring(0, 12 - String(num).length); - }; - - var anchor = function(txt, url){ - url = url ? url : "/"; - return '' + txt + ""; - }; - - var requestHandler = function(request, response){ - //If request matched, send the request; - if(urlMapper.match(request, response)){ - return; - } - - count = 0; - - var url = request.url, - hasQuery = url.indexOf("?"); - - //bug: path.join can't recognize the querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(dir, url), - stat; - - try{ - stat = fs.statSync(fullPath) - }catch(err){ - response.writeHead(404, {"Content-Type": "text/html"}); - response.end("File not found!"); - return; - } - - //List all the files in a directory. - var listFiles = function(callback){ - - fs.readdir(fullPath, function(err, files){ - if(err){ - console.log(err); - return; - } - - for(var idx = 0, len = files.length; idx < len; idx++){ - //persistent the idx before make the sync process - (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - count++; - - if(err){ - console.log(err); - }else{ - response.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - count == len && callback(); - }); - })(idx); - } - }); - }; - - //Is file? Open this file and send to client. - if(stat.isFile()){ - self.writeFile(response, fullPath); - } - //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ - response.writeHead(200, {"Content-Type": "text/html"}); - response.write("

http://localhost:" + port + url + "


"); - response.write("
");
-        response.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
-        listFiles(function(){
-          response.write("

"); - response.end("
Count: " + count + "
"); - }); - } - }; - - /* Expose the urlMapper handle method*/ - self.url = urlMapper.url; - self.post = urlMapper.post; - - self.writeFile = function(response, fullPath){ - fs.readFile(fullPath, function(err, data){ - if(err){ - console.log(err); - return; - } - response.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); - response.end(data, "binary"); - }); - }; - - /* - try write file, we don't know the path is relative or absolute. - */ - self.tryWriteFile = function(response, filePath){ - var stat; - try{ - stat = fs.statSync(filePath) - } - catch(err){ - try{ - filePath = path.join(dir, filePath); - stat = fs.statSync(filePath); - } - catch(e){ - response.writeHead(404, {"Content-Type": "text/html"}); - response.end("File not found!"); - } - } - - self.writeFile(response, filePath); - }; - - /* - public: start http server - */ - self.start = function(){ - // Entry Point - (function(args){ - dir = args[2] || strDir || dir; - port = Number(args[3]) || Number(strPort) || port; - - try{ - //Create http server - var httpSvr = require("http").createServer(requestHandler); - httpSvr.listen(port); - - console.log("Running at localhost" - ,"dir:", dir - ,"Port:", port - ); - - self.httpSvr = httpSvr; - - return true; - } - catch(err){ - console.log("Can't setup server at port", port, err); - } - return false; - })(process.argv); - - }; - - /* - public: close http server; - */ - self.close = function(){ - if(self.httpSvr){ - self.httpSvr.close(); - return true; - } - return false; - }; - - }; - - return server; - -})(); \ No newline at end of file diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 41b3a48c8959e5f7c265c147795fca2543ad0055..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 916624 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

hmNzM0u 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

    OTw(4_TC^iTplCN62n4WgTlCf`sKK3vL86`>4=0 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�u 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

    i?Xk|mpZv1SPCcW^gf< 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℘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 My85}Sb4q9e04Vb@V*mgE diff --git a/svr/Filter.js b/svr/Filter.js new file mode 100644 index 0000000..3d26049 --- /dev/null +++ b/svr/Filter.js @@ -0,0 +1,62 @@ +/* +Http Filter: Execute all the rules that matched, +Filters will be always called before a handler. +*/ +var Filter = { + //filter list + filters : [], + + /* + add a new filter + */ + add : function(regExp, handler, options){ + var params = {regExp: regExp, handler: handler}; + Filter.filters.push(Object.extend(params, options)); + }, + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + file: function(regExp, handler, options){ + var params = {regExp: regExp, handler: handler, file: true}; + //insert as the first elements + Filter.filters.splice(0, 0, Object.extend(params, options)); + } +}; + +/* +Filter Chain +*/ +var FilterChain = function(cb){ + var self = this; + self.idx = 0; + self.cb = cb; +}; + +FilterChain.prototype = { + next : function(req, res){ + var self = this; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if(!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res){ + //filter actions + req.next(req, res); + }, options); + */ + if(mapper.regExp && mapper.regExp.test(req.url)){ + console.log("filter matched", self.idx, mapper.regExp, req.url); + Parser(req, res, mapper); + }else{ + self.next(req, res); + } + } +}; \ No newline at end of file diff --git a/svr/Handler.js b/svr/Handler.js new file mode 100644 index 0000000..b0c696c --- /dev/null +++ b/svr/Handler.js @@ -0,0 +1,86 @@ +/* +Http Handler: Execute and returned when when first matched; +At the same time only one Handler will be called; +*/ +var Handler; + +(function(){ + + /* + Private: web server instance + */ + var webSvr; + + /* + Private: handler list + */ + var handlers = []; + + /* + Static Handler instance + */ + Handler = { + + url : function(regExp, handler, options){ + var params = {regExp: regExp, handler: handler}; + handlers.push(Object.extend(params, options)); + }, + + //Post: Parse the post data by default; + post : function(regExp, handler, options){ + var params = { parse: true }; + this.url(regExp, handler, Object.extend(params, options)); + }, + + //Session: Parse the session and post by default; + session : function(regExp, handler){ + this.url(regExp, handler, { parse:true, session: true }); + }, + + handle : function(req, res){ + //flag: is matched? + for(var i = 0, len = handlers.length; i < len ; i++){ + + var mapper = handlers[i]; + if(mapper.regExp && mapper.regExp.test(req.url)){ + + console.log("handler matched", i, mapper.regExp, req.url); + + try{ + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type){ + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + } + catch(err){ + console.log(err) + } + + return true; + } + } + return false; + } //end of handler + + }; + +}()); + + + diff --git a/svr/MakeFile.list b/svr/MakeFile.list new file mode 100644 index 0000000..d275660 --- /dev/null +++ b/svr/MakeFile.list @@ -0,0 +1,18 @@ +#config file +#These modules will be crunched to one. +#TODO: Need to support dictionary in the list; + +#reference +ref/Math.uuid.js +ref/Object.extend.js + +#WebSvr +RequestParser.js +SessionParser.js +Parser.js +Filter.js +Handler.js +WebSvr.js + +#Testing Site +SiteTest.js \ No newline at end of file diff --git a/svr/Parser.js b/svr/Parser.js new file mode 100644 index 0000000..844f148 --- /dev/null +++ b/svr/Parser.js @@ -0,0 +1,68 @@ +/* +Parser: Functions that Filter and Handler will be called +*/ +var Parser = function(req, res, mapper){ + + var handler = mapper.handler; + + //add sesion support + var parseSession = function(){ + //add sesion support + if(mapper.session && typeof req.session == "undefined"){ + SessionParser(req, res, function(session){ + req.session = session; + handler(req, res); + }); + }else{ + handler(req, res); + } + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseRequest = function(){ + //need to parse the request? + if(mapper.parse && typeof req.body == "undefined"){ + //Must parser the request first, or the post data will lost; + RequestParser(req, res, function(data){ + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function(){ + //Need to parse the file in request? + if(mapper.file && typeof req.body == "undefined"){ + //Must parser the request first, or the post data maybe lost; + var formidable = require('./lib/incoming_form'); + + var form = new formidable.IncomingForm(); + form.parse(req, function(err, fields, files) { + if (err){ + console.log(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseRequest(); + }); + }else{ + parseRequest(); + }; + }; + + parseFile(); + +}; diff --git a/svr/README.md b/svr/README.md new file mode 100644 index 0000000..9485d95 --- /dev/null +++ b/svr/README.md @@ -0,0 +1,94 @@ +WebSvr: Version 0.012 +============== +Lincense: MIT + +version: 0.020 +-------------- +- Filter: A Request will mapping all the filters first, and then pass to the Handler; +- Handler: When a request matched a handler, it will returned, so only one handler will be matched; +- Session: Stored in file, with JSON format; +- Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ + +version: 0.005 +-------------- +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime + +Sample: +-------------- + + //Start WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; + //Trying at: http://localhost:8054 + var webSvr = new WebSvr({root:"./../"}); + webSvr.start(); + + + var fs = require("fs"), + querystring = require("querystring"); + + + /* + Filter: test/* => (session validation function); + parse:parse the post data and stored to req.body; + session: init the session and stored in req.session; + */ + webSvr.filter(/test\/[\w\.]+/, function(req, res){ + //It's not index.htm/login.do, do the session validation + if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ + !req.session.get("username") && res.end("You must login, first!"); + } + + //Link to next filter + req.filter.next(req, res); + }, {parse: true, session: true}); + + + /* + Handler: login.do => (validate the username & password) + username: admin + password: 12345678 + */ + webSvr.url(/login.do/, function(req, res){ + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if(qs.username == "admin" && qs.password == "12345678"){ + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session){ + //TODO: Add req.redirect / req.forward functionalities; + res.writeHead(200, {"Content-Type": "text/html"}); + res.writeFile("/test/setting.htm"); + }); + }else{ + res.writeHead(401); + res.end("Wrong username/password"); + } + }); + + + /* + Uploader: upload.do => (receive handler) + */ + webSvr.file(/upload.do/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); + }); + + + /* + Simple redirect API: + */ + //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine + webSvr.url(/combine/, ["svr/tool/Combine.js"]); + //Mapping "hello" to a string, trying at http://localhost:8054/hello + webSvr.url(/hello/, "Hello WebSvr!"); + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post + webSvr.post(/post/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/html"}); + //Need session support + res.write("You username is " + req.session.get("username")); + res.write('


    '); + res.end('Received : ' + req.body); + }, {session: true}); \ No newline at end of file diff --git a/svr/RequestParser.js b/svr/RequestParser.js new file mode 100644 index 0000000..5784e8c --- /dev/null +++ b/svr/RequestParser.js @@ -0,0 +1,27 @@ +/* +Request parser, parse the data in request body via +when parse complete, execute the callback, with response data; +*/ +var RequestParser; + +(function(){ + + //TODO: Is there a bug, how about 2 users update a file, what's will happened for buffer; + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE); + + RequestParser = function(req, res, callback){ + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + +}()); \ No newline at end of file diff --git a/svr/SessionParser.js b/svr/SessionParser.js new file mode 100644 index 0000000..3635b30 --- /dev/null +++ b/svr/SessionParser.js @@ -0,0 +1,89 @@ + +var SessionParser; + +//TODO: Need a child process of clear session +(function(){ + + var fs = require("fs"); + + SessionParser = (function(req, res, callback){ + + var self = { + //session id + sid : null, + //session stored object + obj : {} + }; + + //TODO + self.set = function(key, val, callback){ + + var sessionfile = 'tmp/session/' + self.sid; + + key && (self.obj[key] = val); + + fs.writeFile( sessionfile, JSON.stringify(self.obj), function(err){ + if(err){ + console.log(err); + return; + } + + callback && callback(self); + }); + }; + + //TO DO + self.get = function(key){ + return self.obj[key]; + }; + + self.init = function(){ + var sidKey = "_wsid", + sidVal, + cookie = req.headers.cookie || ""; + + //Get or Create sid + var idx = cookie.indexOf(sidKey + "="); + + //sid exist in the cookie, read it + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //sid doesn't exist, create it; + if(idx < 0 || sidVal.length != 32){ + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + }; + self.sid = sidVal; + + //We only receive the cookie from Http headers + var sessionfile = 'tmp/session/' + self.sid; + + //here will be cause a bit of delay + fs.exists(sessionfile, function (exists) { + if(exists){ + fs.readFile( sessionfile, function (err, data) { + if (err) { + console.log(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + callback(self); + }); + }else{ + //session not exist create one + self.obj = {}; + self.set(null , null , callback); + } + }); + + }; + + self.init(); + + return self; + + }); + +}()); \ No newline at end of file diff --git a/svr/SiteTest.js b/svr/SiteTest.js new file mode 100644 index 0000000..1aa392c --- /dev/null +++ b/svr/SiteTest.js @@ -0,0 +1,77 @@ + +//Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; +//Trying at: http://localhost:8054 +var webSvr = new WebSvr({root:"./../"}); +webSvr.start(); + + +var fs = require("fs"), + querystring = require("querystring"); + + +/* +Filter: test/* => (session validation function); + parse:parse the post data and stored to req.body; + session: init the session and stored in req.session; +*/ +webSvr.filter(/test\/[\w\.]+/, function(req, res){ + //It's not index.htm/login.do, do the session validation + if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ + !req.session.get("username") && res.end("You must login, first!"); + } + + //Link to next filter + req.filter.next(req, res); +}, {parse: true, session: true}); + + +/* +Handler: login.do => (validate the username & password) + username: admin + password: 12345678 +*/ +webSvr.url(/login.do/, function(req, res){ + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if(qs.username == "admin" && qs.password == "12345678"){ + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session){ + //TODO: Add req.redirect / req.forward functionalities; + res.writeHead(200, {"Content-Type": "text/html"}); + res.writeFile("/test/setting.htm"); + }); + }else{ + res.writeHead(401); + res.end("Wrong username/password"); + } +}); + + +/* +Uploader: upload.do => (receive handler) +*/ +webSvr.file(/upload.do/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); +}); + + +/* +Simple redirect API: +*/ +//Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine +webSvr.url(/combine/, ["svr/tool/Combine.js"]); +//Mapping "hello" to a string, trying at http://localhost:8054/hello +webSvr.url(/hello/, "Hello WebSvr!"); +//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post +webSvr.post(/post/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/html"}); + //Need session support + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); +}, {session: true}); \ No newline at end of file diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js new file mode 100644 index 0000000..b069139 --- /dev/null +++ b/svr/WebSvr.all.js @@ -0,0 +1,780 @@ +/*ref\Math.uuid.js*/ +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); + +/*ref\Object.extend.js*/ +Object.extend = function(des, src){ + + if(!des) des = {}; + + for(var p in src){ + if(!des[p]) des[p] = src[p]; + } + + return des; +} +/*RequestParser.js*/ +/* +Request parser, parse the data in request body via +when parse complete, execute the callback, with response data; +*/ +var RequestParser; + +(function(){ + + //TODO: Is there a bug, how about 2 users update a file, what's will happened for buffer; + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE); + + RequestParser = function(req, res, callback){ + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + +}()); +/*SessionParser.js*/ + +var SessionParser; + +//TODO: Need a child process of clear session +(function(){ + + var fs = require("fs"); + + SessionParser = (function(req, res, callback){ + + var self = { + //session id + sid : null, + //session stored object + obj : {} + }; + + //TODO + self.set = function(key, val, callback){ + + var sessionfile = 'tmp/session/' + self.sid; + + key && (self.obj[key] = val); + + fs.writeFile( sessionfile, JSON.stringify(self.obj), function(err){ + if(err){ + console.log(err); + return; + } + + callback && callback(self); + }); + }; + + //TO DO + self.get = function(key){ + return self.obj[key]; + }; + + self.init = function(){ + var sidKey = "_wsid", + sidVal, + cookie = req.headers.cookie || ""; + + //Get or Create sid + var idx = cookie.indexOf(sidKey + "="); + + //sid exist in the cookie, read it + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //sid doesn't exist, create it; + if(idx < 0 || sidVal.length != 32){ + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + }; + self.sid = sidVal; + + //We only receive the cookie from Http headers + var sessionfile = 'tmp/session/' + self.sid; + + //here will be cause a bit of delay + fs.exists(sessionfile, function (exists) { + if(exists){ + fs.readFile( sessionfile, function (err, data) { + if (err) { + console.log(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + callback(self); + }); + }else{ + //session not exist create one + self.obj = {}; + self.set(null , null , callback); + } + }); + + }; + + self.init(); + + return self; + + }); + +}()); +/*Parser.js*/ +/* +Parser: Functions that Filter and Handler will be called +*/ +var Parser = function(req, res, mapper){ + + var handler = mapper.handler; + + //add sesion support + var parseSession = function(){ + //add sesion support + if(mapper.session && typeof req.session == "undefined"){ + SessionParser(req, res, function(session){ + req.session = session; + handler(req, res); + }); + }else{ + handler(req, res); + } + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseRequest = function(){ + //need to parse the request? + if(mapper.parse && typeof req.body == "undefined"){ + //Must parser the request first, or the post data will lost; + RequestParser(req, res, function(data){ + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function(){ + //Need to parse the file in request? + if(mapper.file && typeof req.body == "undefined"){ + //Must parser the request first, or the post data maybe lost; + var formidable = require('./lib/incoming_form'); + + var form = new formidable.IncomingForm(); + form.parse(req, function(err, fields, files) { + if (err){ + console.log(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseRequest(); + }); + }else{ + parseRequest(); + }; + }; + + parseFile(); + +}; + +/*Filter.js*/ +/* +Http Filter: Execute all the rules that matched, +Filters will be always called before a handler. +*/ +var Filter = { + //filter list + filters : [], + + /* + add a new filter + */ + add : function(regExp, handler, options){ + var params = {regExp: regExp, handler: handler}; + Filter.filters.push(Object.extend(params, options)); + }, + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + file: function(regExp, handler, options){ + var params = {regExp: regExp, handler: handler, file: true}; + //insert as the first elements + Filter.filters.splice(0, 0, Object.extend(params, options)); + } +}; + +/* +Filter Chain +*/ +var FilterChain = function(cb){ + var self = this; + self.idx = 0; + self.cb = cb; +}; + +FilterChain.prototype = { + next : function(req, res){ + var self = this; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if(!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res){ + //filter actions + req.next(req, res); + }, options); + */ + if(mapper.regExp && mapper.regExp.test(req.url)){ + console.log("filter matched", self.idx, mapper.regExp, req.url); + Parser(req, res, mapper); + }else{ + self.next(req, res); + } + } +}; +/*Handler.js*/ +/* +Http Handler: Execute and returned when when first matched; +At the same time only one Handler will be called; +*/ +var Handler; + +(function(){ + + /* + Private: web server instance + */ + var webSvr; + + /* + Private: handler list + */ + var handlers = []; + + /* + Static Handler instance + */ + Handler = { + + url : function(regExp, handler, options){ + var params = {regExp: regExp, handler: handler}; + handlers.push(Object.extend(params, options)); + }, + + //Post: Parse the post data by default; + post : function(regExp, handler, options){ + var params = { parse: true }; + this.url(regExp, handler, Object.extend(params, options)); + }, + + //Session: Parse the session and post by default; + session : function(regExp, handler){ + this.url(regExp, handler, { parse:true, session: true }); + }, + + handle : function(req, res){ + //flag: is matched? + for(var i = 0, len = handlers.length; i < len ; i++){ + + var mapper = handlers[i]; + if(mapper.regExp && mapper.regExp.test(req.url)){ + + console.log("handler matched", i, mapper.regExp, req.url); + + try{ + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type){ + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + } + catch(err){ + console.log(err) + } + + return true; + } + } + return false; + } //end of handler + + }; + +}()); + + + + +/*WebSvr.js*/ +/* +* Description: Create a static file server (http based). +* This will list all the files and directories via Node.Js. +* The behavior will be like directory browsing enabled in IIS, +* Author: Kris Zhang +* Dependence: Node.js: http://www.nodejs.org, +* mime.js: https://github.com/bentomas/node-mime +* Math.uuid.js (v1.4) : http://www.broofa.com +* Date: 2012-3 Draft +* 2012-4 Update: Using async and mime.js +* 2012-7 Update: Rename and reformat files +*/ +/* +* WebSvr Namespace +*/ +var WebSvr = (function(){ + + var defaults = { + //Server port + port: 8054, + //Root path + root: "./../web", + session: false, + }; + + var server = function(options){ + //Library + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"); + + //Parameters + //Count: How many files? + var self = this, + count = 0, + root, + port; + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + var anchor = function(txt, url){ + url = url ? url : "/"; + return '
    ' + txt + ""; + }; + + var fileHandler = function(req, res){ + + var url = req.url, + hasQuery = url.indexOf("?"); + + //Bug: path.join can't recognize the querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; + + var fullPath = path.join(root, url); + + fs.stat(fullPath, function(err, stat){ + + count = 0; + + //Consider as file not found + if(err) return self.write404(res); + + //List all the files in a directory. + var listFiles = function(callback){ + + fs.readdir(fullPath, function(err, files){ + if(err){ + console.log(err); + return; + } + + for(var idx = 0, len = files.length; idx < len; idx++){ + //Persistent the idx before make the sync process + (function(idx){ + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + count++; + + if(err){ + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + count == len && callback(); + }); + })(idx); + } + + //If it's an empty directory + (len == 0) && callback(); + }); + }; + + //Is file? Open this file and send to client. + if(stat.isFile()){ + writeFile(res, fullPath); + } + + //Is Directory? List all the files and folders. + else if(stat.isDirectory()){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +          listFiles(function(){
    +            res.write("

    "); + res.end("
    Count: " + count + "
    "); + }); + } + + }); + }; + + var requestHandler = function(req, res){ + //Response may be shutdown when do the filter, in order not to cause exception, + //Rewrite the write/writeHead functionalities of current response object + var endFn = res.end; + res.end = function(){ + //Execute old end; + endFn.apply(res, arguments); + //Rewirte write/writeHead on response object + res.write = res.writeHead = function(){ + console.log("response is already end, response write ignored!") + }; + }; + + res.writeFile = function(filePath, cb){ + self.writeFile(res, filePath, cb); + }; + + //Define filter object + req.filter = new FilterChain(function(){ + //if handler not match, send the request + !Handler.handle(req, res) && fileHandler(req, res); + }); + + //Handle the first filter + req.filter.next(req, res); + }; + + var writeFile = function(res, fullPath){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + res.end(data, "binary"); + }); + }; + + //Explose API + //Filter + self.filter = Filter.add; + self.file = Filter.file; + + //Handler + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; + + //Get a fullpath of a request + self.getFullPath = function(filePath){ + return path.join(root, filePath); + }; + + //Write file, filePath is a relative; + self.writeFile = function(res, filePath, cb){ + filePath = path.join(root, filePath); + fs.exists(filePath, function(exist){ + if(exist){ + writeFile(res, filePath); + cb && cb(exist); + }else{ + //If callback function doesn't exist, write 404 page; + cb ? cb(exist) : self.write404(res); + } + }); + }; + + self.write404 = function(res){ + res.writeHead(404, {"Content-Type": "text/html"}); + res.end("File not found!"); + }; + + //Public: start http server + self.start = function(){ + options = options || {}; + + Object.extend(options, defaults); + + root = options.root; + port = parseInt(options.port); + + //Expose the API + self.options = options; + + try{ + //Create http server + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Running at" + ,"Root:", root + ,"Port:", port + ); + + self.httpSvr = httpSvr; + + return true; + } + catch(err){ + console.log("Can't setup server at port", port, err); + } + return false; + }; + + //Public: close http server; + self.close = function(){ + if(self.httpSvr){ + self.httpSvr.close(); + return true; + } + return false; + }; + + }; + + return server; + +})(); +/*SiteTest.js*/ + +//Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; +//Trying at: http://localhost:8054 +var webSvr = new WebSvr({root:"./../"}); +webSvr.start(); + + +var fs = require("fs"), + querystring = require("querystring"); + + +/* +Filter: test/* => (session validation function); + parse:parse the post data and stored to req.body; + session: init the session and stored in req.session; +*/ +webSvr.filter(/test\/[\w\.]+/, function(req, res){ + //It's not index.htm/login.do, do the session validation + if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ + !req.session.get("username") && res.end("You must login, first!"); + } + + //Link to next filter + req.filter.next(req, res); +}, {parse: true, session: true}); + + +/* +Handler: login.do => (validate the username & password) + username: admin + password: 12345678 +*/ +webSvr.url(/login.do/, function(req, res){ + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if(qs.username == "admin" && qs.password == "12345678"){ + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session){ + //TODO: Add req.redirect / req.forward functionalities; + res.writeHead(200, {"Content-Type": "text/html"}); + res.writeFile("/test/setting.htm"); + }); + }else{ + res.writeHead(401); + res.end("Wrong username/password"); + } +}); + + +/* +Uploader: upload.do => (receive handler) +*/ +webSvr.file(/upload.do/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); +}); + + +/* +Simple redirect API: +*/ +//Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine +webSvr.url(/combine/, ["svr/tool/Combine.js"]); +//Mapping "hello" to a string, trying at http://localhost:8054/hello +webSvr.url(/hello/, "Hello WebSvr!"); +//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post +webSvr.post(/post/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/html"}); + //Need session support + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); +}, {session: true}); diff --git a/svr/WebSvr.js b/svr/WebSvr.js new file mode 100644 index 0000000..feb49c7 --- /dev/null +++ b/svr/WebSvr.js @@ -0,0 +1,257 @@ +/* +* Description: Create a static file server (http based). +* This will list all the files and directories via Node.Js. +* The behavior will be like directory browsing enabled in IIS, +* Author: Kris Zhang +* Dependence: Node.js: http://www.nodejs.org, +* mime.js: https://github.com/bentomas/node-mime +* Math.uuid.js (v1.4) : http://www.broofa.com +* Date: 2012-3 Draft +* 2012-4 Update: Using async and mime.js +* 2012-7 Update: Rename and reformat files +*/ +/* +* WebSvr Namespace +*/ +var WebSvr = (function(){ + + var defaults = { + //Server port + port: 8054, + //Root path + root: "./../web", + session: false, + }; + + var server = function(options){ + //Library + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"); + + //Parameters + //Count: How many files? + var self = this, + count = 0, + root, + port; + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + var anchor = function(txt, url){ + url = url ? url : "/"; + return '' + txt + ""; + }; + + var fileHandler = function(req, res){ + + var url = req.url, + hasQuery = url.indexOf("?"); + + //Bug: path.join can't recognize the querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; + + var fullPath = path.join(root, url); + + fs.stat(fullPath, function(err, stat){ + + count = 0; + + //Consider as file not found + if(err) return self.write404(res); + + //List all the files in a directory. + var listFiles = function(callback){ + + fs.readdir(fullPath, function(err, files){ + if(err){ + console.log(err); + return; + } + + for(var idx = 0, len = files.length; idx < len; idx++){ + //Persistent the idx before make the sync process + (function(idx){ + var filePath = path.join(fullPath, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + count++; + + if(err){ + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + count == len && callback(); + }); + })(idx); + } + + //If it's an empty directory + (len == 0) && callback(); + }); + }; + + //Is file? Open this file and send to client. + if(stat.isFile()){ + writeFile(res, fullPath); + } + + //Is Directory? List all the files and folders. + else if(stat.isDirectory()){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +          listFiles(function(){
    +            res.write("

    "); + res.end("
    Count: " + count + "
    "); + }); + } + + }); + }; + + var requestHandler = function(req, res){ + //Response may be shutdown when do the filter, in order not to cause exception, + //Rewrite the write/writeHead functionalities of current response object + var endFn = res.end; + res.end = function(){ + //Execute old end; + endFn.apply(res, arguments); + //Rewirte write/writeHead on response object + res.write = res.writeHead = function(){ + console.log("response is already end, response write ignored!") + }; + }; + + res.writeFile = function(filePath, cb){ + self.writeFile(res, filePath, cb); + }; + + //Define filter object + req.filter = new FilterChain(function(){ + //if handler not match, send the request + !Handler.handle(req, res) && fileHandler(req, res); + }); + + //Handle the first filter + req.filter.next(req, res); + }; + + var writeFile = function(res, fullPath){ + fs.readFile(fullPath, function(err, data){ + if(err){ + console.log(err); + return; + } + res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + res.end(data, "binary"); + }); + }; + + //Explose API + //Filter + self.filter = Filter.add; + self.file = Filter.file; + + //Handler + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; + + //Get a fullpath of a request + self.getFullPath = function(filePath){ + return path.join(root, filePath); + }; + + //Write file, filePath is a relative; + self.writeFile = function(res, filePath, cb){ + filePath = path.join(root, filePath); + fs.exists(filePath, function(exist){ + if(exist){ + writeFile(res, filePath); + cb && cb(exist); + }else{ + //If callback function doesn't exist, write 404 page; + cb ? cb(exist) : self.write404(res); + } + }); + }; + + self.write404 = function(res){ + res.writeHead(404, {"Content-Type": "text/html"}); + res.end("File not found!"); + }; + + //Public: start http server + self.start = function(){ + options = options || {}; + + Object.extend(options, defaults); + + root = options.root; + port = parseInt(options.port); + + //Expose the API + self.options = options; + + try{ + //Create http server + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Running at" + ,"Root:", root + ,"Port:", port + ); + + self.httpSvr = httpSvr; + + return true; + } + catch(err){ + console.log("Can't setup server at port", port, err); + } + return false; + }; + + //Public: close http server; + self.close = function(){ + if(self.httpSvr){ + self.httpSvr.close(); + return true; + } + return false; + }; + + }; + + return server; + +})(); \ No newline at end of file diff --git a/build.bat b/svr/build.bat similarity index 59% rename from build.bat rename to svr/build.bat index 4754c1d..4edeaef 100644 --- a/build.bat +++ b/svr/build.bat @@ -1,6 +1,11 @@ REM Combine first node tool/Combine.js -i makefile.list -o WebSvr.all.js + REM Then excute it +:start node WebSvr.all.js -REM Complete Gooldbye. + +REM When error occured, restart the server. +REM goto start + pause; \ No newline at end of file diff --git a/svr/lib/file.js b/svr/lib/file.js new file mode 100644 index 0000000..362019c --- /dev/null +++ b/svr/lib/file.js @@ -0,0 +1,73 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +var util = require('./util'), + WriteStream = require('fs').WriteStream, + EventEmitter = require('events').EventEmitter, + crypto = require('crypto'); + +function File(properties) { + EventEmitter.call(this); + + this.size = 0; + this.path = null; + this.name = null; + this.type = null; + this.hash = null; + this.lastModifiedDate = null; + + this._writeStream = null; + + for (var key in properties) { + this[key] = properties[key]; + } + + if(typeof this.hash === 'string') { + this.hash = crypto.createHash(properties.hash); + } + + this._backwardsCompatibility(); +} +module.exports = File; +util.inherits(File, EventEmitter); + +// @todo Next release: Show error messages when accessing these +File.prototype._backwardsCompatibility = function() { + var self = this; + this.__defineGetter__('length', function() { + return self.size; + }); + this.__defineGetter__('filename', function() { + return self.name; + }); + this.__defineGetter__('mime', function() { + return self.type; + }); +}; + +File.prototype.open = function() { + this._writeStream = new WriteStream(this.path); +}; + +File.prototype.write = function(buffer, cb) { + var self = this; + this._writeStream.write(buffer, function() { + if(self.hash) { + self.hash.update(buffer); + } + self.lastModifiedDate = new Date(); + self.size += buffer.length; + self.emit('progress', self.size); + cb(); + }); +}; + +File.prototype.end = function(cb) { + var self = this; + this._writeStream.end(function() { + if(self.hash) { + self.hash = self.hash.digest('hex'); + } + self.emit('end'); + cb(); + }); +}; diff --git a/svr/lib/incoming_form.js b/svr/lib/incoming_form.js new file mode 100644 index 0000000..76eb944 --- /dev/null +++ b/svr/lib/incoming_form.js @@ -0,0 +1,390 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +var fs = require('fs'); +var util = require('./util'), + path = require('path'), + File = require('./file'), + MultipartParser = require('./multipart_parser').MultipartParser, + QuerystringParser = require('./querystring_parser').QuerystringParser, + StringDecoder = require('string_decoder').StringDecoder, + EventEmitter = require('events').EventEmitter, + Stream = require('stream').Stream; + +function IncomingForm(opts) { + if (!(this instanceof IncomingForm)) return new IncomingForm; + EventEmitter.call(this); + + opts=opts||{}; + + this.error = null; + this.ended = false; + + this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; + this.keepExtensions = opts.keepExtensions || false; + this.uploadDir = opts.uploadDir || IncomingForm.UPLOAD_DIR; + this.encoding = opts.encoding || 'utf-8'; + this.headers = null; + this.type = null; + this.hash = false; + + this.bytesReceived = null; + this.bytesExpected = null; + + this._parser = null; + this._flushing = 0; + this._fieldsSize = 0; +}; +util.inherits(IncomingForm, EventEmitter); +exports.IncomingForm = IncomingForm; + +IncomingForm.UPLOAD_DIR = (function() { + var dirs = [ + process.env.TMP, + process.env.TMPDIR, + process.env.TEMP, + '/tmp', + process.cwd() + ]; + for (var i = 0; i < dirs.length; i++) { + var dir = dirs[i]; + var isDirectory = false; + + try { + isDirectory = fs.statSync(dir).isDirectory(); + } catch (e) {} + + if (isDirectory) return dir; + } +})(); + +IncomingForm.prototype.parse = function(req, cb) { + this.pause = function() { + try { + req.pause(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + return true; + }; + + this.resume = function() { + try { + req.resume(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + + return true; + }; + + this.writeHeaders(req.headers); + + var self = this; + req + .on('error', function(err) { + self._error(err); + }) + .on('aborted', function() { + self.emit('aborted'); + }) + .on('data', function(buffer) { + self.write(buffer); + }) + .on('end', function() { + if (self.error) { + return; + } + + var err = self._parser.end(); + if (err) { + self._error(err); + } + }); + + if (cb) { + var fields = {}, files = {}; + this + .on('field', function(name, value) { + fields[name] = value; + }) + .on('file', function(name, file) { + files[name] = file; + }) + .on('error', function(err) { + cb(err, fields, files); + }) + .on('end', function() { + cb(null, fields, files); + }); + } + + return this; +}; + +IncomingForm.prototype.writeHeaders = function(headers) { + this.headers = headers; + this._parseContentLength(); + this._parseContentType(); +}; + +IncomingForm.prototype.write = function(buffer) { + if (!this._parser) { + this._error(new Error('unintialized parser')); + return; + } + + this.bytesReceived += buffer.length; + this.emit('progress', this.bytesReceived, this.bytesExpected); + + var bytesParsed = this._parser.write(buffer); + if (bytesParsed !== buffer.length) { + this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); + } + + return bytesParsed; +}; + +IncomingForm.prototype.pause = function() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; +}; + +IncomingForm.prototype.resume = function() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; +}; + +IncomingForm.prototype.onPart = function(part) { + // this method can be overwritten by the user + this.handlePart(part); +}; + +IncomingForm.prototype.handlePart = function(part) { + var self = this; + + if (part.filename === undefined) { + var value = '' + , decoder = new StringDecoder(this.encoding); + + part.on('data', function(buffer) { + self._fieldsSize += buffer.length; + if (self._fieldsSize > self.maxFieldsSize) { + self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); + return; + } + value += decoder.write(buffer); + }); + + part.on('end', function() { + self.emit('field', part.name, value); + }); + return; + } + + this._flushing++; + + var file = new File({ + path: this._uploadPath(part.filename), + name: part.filename, + type: part.mime, + hash: self.hash + }); + + this.emit('fileBegin', part.name, file); + + file.open(); + + part.on('data', function(buffer) { + self.pause(); + file.write(buffer, function() { + self.resume(); + }); + }); + + part.on('end', function() { + file.end(function() { + self._flushing--; + self.emit('file', part.name, file); + self._maybeEnd(); + }); + }); +}; + +IncomingForm.prototype._parseContentType = function() { + if (!this.headers['content-type']) { + this._error(new Error('bad content-type header, no content-type')); + return; + } + + if (this.headers['content-type'].match(/urlencoded/i)) { + this._initUrlencoded(); + return; + } + + if (this.headers['content-type'].match(/multipart/i)) { + var m; + if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { + this._initMultipart(m[1] || m[2]); + } else { + this._error(new Error('bad content-type header, no multipart boundary')); + } + return; + } + + this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); +}; + +IncomingForm.prototype._error = function(err) { + if (this.error) { + return; + } + + this.error = err; + this.pause(); + this.emit('error', err); +}; + +IncomingForm.prototype._parseContentLength = function() { + if (this.headers['content-length']) { + this.bytesReceived = 0; + this.bytesExpected = parseInt(this.headers['content-length'], 10); + this.emit('progress', this.bytesReceived, this.bytesExpected); + } +}; + +IncomingForm.prototype._newParser = function() { + return new MultipartParser(); +}; + +IncomingForm.prototype._initMultipart = function(boundary) { + this.type = 'multipart'; + + var parser = new MultipartParser(), + self = this, + headerField, + headerValue, + part; + + parser.initWithBoundary(boundary); + + parser.onPartBegin = function() { + part = new Stream(); + part.readable = true; + part.headers = {}; + part.name = null; + part.filename = null; + part.mime = null; + headerField = ''; + headerValue = ''; + }; + + parser.onHeaderField = function(b, start, end) { + headerField += b.toString(self.encoding, start, end); + }; + + parser.onHeaderValue = function(b, start, end) { + headerValue += b.toString(self.encoding, start, end); + }; + + parser.onHeaderEnd = function() { + headerField = headerField.toLowerCase(); + part.headers[headerField] = headerValue; + + var m; + if (headerField == 'content-disposition') { + if (m = headerValue.match(/name="([^"]+)"/i)) { + part.name = m[1]; + } + + part.filename = self._fileName(headerValue); + } else if (headerField == 'content-type') { + part.mime = headerValue; + } + + headerField = ''; + headerValue = ''; + }; + + parser.onHeadersEnd = function() { + self.onPart(part); + }; + + parser.onPartData = function(b, start, end) { + part.emit('data', b.slice(start, end)); + }; + + parser.onPartEnd = function() { + part.emit('end'); + }; + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._fileName = function(headerValue) { + var m = headerValue.match(/filename="(.*?)"($|; )/i) + if (!m) return; + + var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); + filename = filename.replace(/%22/g, '"'); + filename = filename.replace(/&#([\d]{4});/g, function(m, code) { + return String.fromCharCode(code); + }); + return filename; +}; + +IncomingForm.prototype._initUrlencoded = function() { + this.type = 'urlencoded'; + + var parser = new QuerystringParser() + , self = this; + + parser.onField = function(key, val) { + self.emit('field', key, val); + }; + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._uploadPath = function(filename) { + var name = ''; + for (var i = 0; i < 32; i++) { + name += Math.floor(Math.random() * 16).toString(16); + } + + if (this.keepExtensions) { + var ext = path.extname(filename); + ext = ext.replace(/(\.[a-z0-9]+).*/, '$1') + + name += ext; + } + + return path.join(this.uploadDir, name); +}; + +IncomingForm.prototype._maybeEnd = function() { + if (!this.ended || this._flushing) { + return; + } + + this.emit('end'); +}; diff --git a/lib/mime.js b/svr/lib/mime.js similarity index 100% rename from lib/mime.js rename to svr/lib/mime.js diff --git a/svr/lib/multipart_parser.js b/svr/lib/multipart_parser.js new file mode 100644 index 0000000..f73140c --- /dev/null +++ b/svr/lib/multipart_parser.js @@ -0,0 +1,312 @@ +var Buffer = require('buffer').Buffer, + s = 0, + S = + { PARSER_UNINITIALIZED: 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, + Z = 122, + + lower = function(c) { + return c | 0x20; + }; + +for (var s in S) { + exports[s] = S[s]; +} + +function MultipartParser() { + this.boundary = null; + this.boundaryChars = null; + this.lookbehind = null; + this.state = S.PARSER_UNINITIALIZED; + + this.index = null; + this.flags = 0; +}; +exports.MultipartParser = MultipartParser; + +MultipartParser.stateToString = function(stateNumber) { + for (var state in S) { + var number = S[state]; + if (number === stateNumber) return state; + } +}; + +MultipartParser.prototype.initWithBoundary = function(str) { + this.boundary = new Buffer(str.length+4); + this.boundary.write('\r\n--', 'ascii', 0); + this.boundary.write(str, 'ascii', 4); + this.lookbehind = new Buffer(this.boundary.length+8); + this.state = S.START; + + this.boundaryChars = {}; + for (var i = 0; i < this.boundary.length; i++) { + this.boundaryChars[this.boundary[i]] = true; + } +}; + +MultipartParser.prototype.write = function(buffer) { + var self = this, + i = 0, + len = buffer.length, + prevIndex = this.index, + index = this.index, + state = this.state, + flags = this.flags, + lookbehind = this.lookbehind, + boundary = this.boundary, + boundaryChars = this.boundaryChars, + boundaryLength = this.boundary.length, + boundaryEnd = boundaryLength - 1, + bufferLength = buffer.length, + c, + cl, + + mark = function(name) { + self[name+'Mark'] = i; + }, + clear = function(name) { + delete self[name+'Mark']; + }, + callback = function(name, buffer, start, end) { + if (start !== undefined && start === end) { + return; + } + + var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); + if (callbackSymbol in self) { + self[callbackSymbol](buffer, start, end); + } + }, + dataCallback = function(name, clear) { + var markSymbol = name+'Mark'; + if (!(markSymbol in self)) { + return; + } + + if (!clear) { + callback(name, buffer, self[markSymbol], buffer.length); + self[markSymbol] = 0; + } else { + callback(name, buffer, self[markSymbol], i); + delete self[markSymbol]; + } + }; + + for (i = 0; i < len; i++) { + c = buffer[i]; + switch (state) { + case S.PARSER_UNINITIALIZED: + return i; + case S.START: + index = 0; + state = S.START_BOUNDARY; + case S.START_BOUNDARY: + if (index == boundary.length - 2) { + if (c != CR) { + return i; + } + index++; + break; + } else if (index - 1 == boundary.length - 2) { + if (c != LF) { + return i; + } + index = 0; + 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; + mark('headerField'); + index = 0; + case S.HEADER_FIELD: + if (c == CR) { + clear('headerField'); + state = S.HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c == HYPHEN) { + break; + } + + if (c == COLON) { + if (index == 1) { + // empty header field + return i; + } + dataCallback('headerField', true); + state = S.HEADER_VALUE_START; + break; + } + + cl = lower(c); + if (cl < A || cl > Z) { + return i; + } + break; + case S.HEADER_VALUE_START: + if (c == SPACE) { + break; + } + + mark('headerValue'); + state = S.HEADER_VALUE; + case S.HEADER_VALUE: + if (c == CR) { + dataCallback('headerValue', true); + 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; + } + + callback('headersEnd'); + state = S.PART_DATA_START; + break; + case S.PART_DATA_START: + state = S.PART_DATA + mark('partData'); + 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 < boundary.length) { + if (boundary[index] == c) { + if (index == 0) { + dataCallback('partData', true); + } + index++; + } else { + index = 0; + } + } else if (index == boundary.length) { + 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 == boundary.length) { + if (flags & F.PART_BOUNDARY) { + index = 0; + if (c == LF) { + // unset the PART_BOUNDARY flag + flags &= ~F.PART_BOUNDARY; + callback('partEnd'); + callback('partBegin'); + state = S.HEADER_FIELD_START; + break; + } + } else if (flags & F.LAST_BOUNDARY) { + if (c == HYPHEN) { + callback('partEnd'); + 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 + callback('partData', lookbehind, 0, prevIndex); + prevIndex = 0; + mark('partData'); + + // 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; + } + } + + dataCallback('headerField'); + dataCallback('headerValue'); + dataCallback('partData'); + + this.index = index; + this.state = state; + this.flags = flags; + + return len; +}; + +MultipartParser.prototype.end = function() { + if (this.state != S.END) { + return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); + } +}; + +MultipartParser.prototype.explain = function() { + return 'state = ' + MultipartParser.stateToString(this.state); +}; diff --git a/svr/lib/querystring_parser.js b/svr/lib/querystring_parser.js new file mode 100644 index 0000000..b8e86e3 --- /dev/null +++ b/svr/lib/querystring_parser.js @@ -0,0 +1,25 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +// This is a buffering parser, not quite as nice as the multipart one. +// If I find time I'll rewrite this to be fully streaming as well +var querystring = require('querystring'); + +function QuerystringParser() { + this.buffer = ''; +}; +exports.QuerystringParser = QuerystringParser; + +QuerystringParser.prototype.write = function(buffer) { + this.buffer += buffer.toString('ascii'); + return buffer.length; +}; + +QuerystringParser.prototype.end = function() { + var fields = querystring.parse(this.buffer); + for (var field in fields) { + this.onField(field, fields[field]); + } + this.buffer = ''; + + this.onEnd(); +}; \ No newline at end of file diff --git a/lib/types/mime.types b/svr/lib/types/mime.types similarity index 100% rename from lib/types/mime.types rename to svr/lib/types/mime.types diff --git a/lib/types/node.types b/svr/lib/types/node.types similarity index 100% rename from lib/types/node.types rename to svr/lib/types/node.types diff --git a/svr/lib/util.js b/svr/lib/util.js new file mode 100644 index 0000000..61f6b4c --- /dev/null +++ b/svr/lib/util.js @@ -0,0 +1,6 @@ +// Backwards compatibility ... +try { + module.exports = require('util'); +} catch (e) { + module.exports = require('sys'); +} diff --git a/svr/ref/Math.uuid.js b/svr/ref/Math.uuid.js new file mode 100644 index 0000000..7538e67 --- /dev/null +++ b/svr/ref/Math.uuid.js @@ -0,0 +1,92 @@ +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); diff --git a/svr/ref/Object.extend.js b/svr/ref/Object.extend.js new file mode 100644 index 0000000..13254a8 --- /dev/null +++ b/svr/ref/Object.extend.js @@ -0,0 +1,10 @@ +Object.extend = function(des, src){ + + if(!des) des = {}; + + for(var p in src){ + if(!des[p]) des[p] = src[p]; + } + + return des; +} \ No newline at end of file diff --git a/tool/Combine.js b/svr/tool/Combine.js similarity index 100% rename from tool/Combine.js rename to svr/tool/Combine.js diff --git a/test/index.htm b/test/index.htm new file mode 100644 index 0000000..6cfc301 --- /dev/null +++ b/test/index.htm @@ -0,0 +1,31 @@ + + + + + + Login + + + +

    WebSvr configuration login:

    +
    +
      +
    • + + + admin +
    • +
    • + + + 12345678 +
    • +
    • + + +
    • +
    +
    + + + diff --git a/test/setting.htm b/test/setting.htm new file mode 100644 index 0000000..39ed103 --- /dev/null +++ b/test/setting.htm @@ -0,0 +1,17 @@ + + + + + + Login + + +
    +
    + + +
    +
    + + + From b059600d6781de4b6a7e078f3a7a3a9a1b2fede8 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 23 Oct 2012 09:46:15 +0800 Subject: [PATCH 007/195] version: 0.020 Add session stored folder; --- svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r | 1 + 1 file changed, 1 insertion(+) create mode 100644 svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r diff --git a/svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r b/svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r new file mode 100644 index 0000000..42adc36 --- /dev/null +++ b/svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r @@ -0,0 +1 @@ +{"username":"admin"} \ No newline at end of file From a7a73556b749563c58450813241c2348046eb5fe Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 23 Oct 2012 09:59:36 +0800 Subject: [PATCH 008/195] Version: 0.020 Add README --- svr/README | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++ svr/README.md | 94 -------------------------------------------- 2 files changed, 105 insertions(+), 94 deletions(-) create mode 100644 svr/README delete mode 100644 svr/README.md diff --git a/svr/README b/svr/README new file mode 100644 index 0000000..738d20f --- /dev/null +++ b/svr/README @@ -0,0 +1,105 @@ +

    WebSvr: Version 0.012

    +

    Lincense: MIT

    + +

    version: 0.020

    +
      +
    • + Filter: A Request will mapping all the filters first, and then pass to the Handler; +
    • +
    • + Handler: When a request matched a handler, it will returned, so only one handler will be matched; +
    • +
    • + Session: Stored in file, with JSON format; +
    • +
    • + Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ +
    • +
    + +

    version: 0.005

    +
      +
    • + MIME: Suppor mime header, integrate https://github.com/broofa/node-mime +
    • +
    + +

    Sample:

    +
    +  //Start WebSVr, runnting at parent folder, default port is 8054, directory browser enabled;
    +  //Trying at: http://localhost:8054
    +  var webSvr = new WebSvr({root:"./../"});
    +  webSvr.start();
    +
    +
    +  var	fs = require("fs"),
    +      querystring = require("querystring");
    +
    +
    +  /*
    +  Filter: test/* => (session validation function);
    +    parse:parse the post data and stored to req.body;
    +    session: init the session and stored in req.session; 
    +  */
    +  webSvr.filter(/test\/[\w\.]+/, function(req, res){
    +    //It's not index.htm/login.do, do the session validation
    +    if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){
    +      !req.session.get("username") && res.end("You must login, first!");
    +    }
    +
    +    //Link to next filter
    +    req.filter.next(req, res);
    +  }, {parse: true, session: true});
    +
    +
    +  /*
    +  Handler: login.do => (validate the username & password)
    +    username: admin
    +    password: 12345678
    +  */
    +  webSvr.url(/login.do/, function(req, res){
    +    //TODO: Add an parameter to auto-complete querystring.parse(req.body);
    +    var qs = querystring.parse(req.body);
    +    if(qs.username == "admin" && qs.password == "12345678"){
    +      //Put key/value pair in session
    +      //TODO: Support put JSON object directly
    +      req.session.set("username", qs.username, function(session){
    +        //TODO: Add req.redirect / req.forward functionalities;
    +        res.writeHead(200, {"Content-Type": "text/html"});
    +        res.writeFile("/test/setting.htm");
    +      });
    +    }else{
    +      res.writeHead(401);
    +      res.end("Wrong username/password");
    +    }
    +  });
    +
    +
    +  /*
    +  Uploader: upload.do => (receive handler)
    +  */
    +  webSvr.file(/upload.do/, function(req, res){
    +    res.writeHead(200, {"Content-Type": "text/plain"});
    +    //Upload file is stored in req.files
    +    //form fields is stored in req.body
    +    res.write(JSON.stringify(req.body));
    +    res.end(JSON.stringify(req.files));
    +  });
    +
    +
    +  /*
    +  Simple redirect API:
    +  */
    +  //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine
    +  webSvr.url(/combine/, ["svr/tool/Combine.js"]);
    +  //Mapping "hello" to a string, trying at http://localhost:8054/hello
    +  webSvr.url(/hello/, "Hello WebSvr!");
    +  //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post
    +  webSvr.post(/post/, function(req, res){
    +    res.writeHead(200, {"Content-Type": "text/html"});
    +    //Need session support
    +    res.write("You username is " + req.session.get("username"));
    +    res.write('

    '); + res.end('Received : ' + req.body); + }, {session: true}); +
    \ No newline at end of file diff --git a/svr/README.md b/svr/README.md deleted file mode 100644 index 9485d95..0000000 --- a/svr/README.md +++ /dev/null @@ -1,94 +0,0 @@ -WebSvr: Version 0.012 -============== -Lincense: MIT - -version: 0.020 --------------- -- Filter: A Request will mapping all the filters first, and then pass to the Handler; -- Handler: When a request matched a handler, it will returned, so only one handler will be matched; -- Session: Stored in file, with JSON format; -- Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ - -version: 0.005 --------------- -- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime - -Sample: --------------- - - //Start WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; - //Trying at: http://localhost:8054 - var webSvr = new WebSvr({root:"./../"}); - webSvr.start(); - - - var fs = require("fs"), - querystring = require("querystring"); - - - /* - Filter: test/* => (session validation function); - parse:parse the post data and stored to req.body; - session: init the session and stored in req.session; - */ - webSvr.filter(/test\/[\w\.]+/, function(req, res){ - //It's not index.htm/login.do, do the session validation - if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ - !req.session.get("username") && res.end("You must login, first!"); - } - - //Link to next filter - req.filter.next(req, res); - }, {parse: true, session: true}); - - - /* - Handler: login.do => (validate the username & password) - username: admin - password: 12345678 - */ - webSvr.url(/login.do/, function(req, res){ - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); - if(qs.username == "admin" && qs.password == "12345678"){ - //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session){ - //TODO: Add req.redirect / req.forward functionalities; - res.writeHead(200, {"Content-Type": "text/html"}); - res.writeFile("/test/setting.htm"); - }); - }else{ - res.writeHead(401); - res.end("Wrong username/password"); - } - }); - - - /* - Uploader: upload.do => (receive handler) - */ - webSvr.file(/upload.do/, function(req, res){ - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); - }); - - - /* - Simple redirect API: - */ - //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine - webSvr.url(/combine/, ["svr/tool/Combine.js"]); - //Mapping "hello" to a string, trying at http://localhost:8054/hello - webSvr.url(/hello/, "Hello WebSvr!"); - //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post - webSvr.post(/post/, function(req, res){ - res.writeHead(200, {"Content-Type": "text/html"}); - //Need session support - res.write("You username is " + req.session.get("username")); - res.write('

    '); - res.end('Received : ' + req.body); - }, {session: true}); \ No newline at end of file From 0f16e72e0a464bd7da68939712a8563680cf0733 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 23 Oct 2012 10:06:15 +0800 Subject: [PATCH 009/195] Version: 0.020 Add README --- svr/README | 171 +++++++++++++++++++++++++---------------------------- 1 file changed, 80 insertions(+), 91 deletions(-) diff --git a/svr/README b/svr/README index 738d20f..12fd597 100644 --- a/svr/README +++ b/svr/README @@ -1,105 +1,94 @@ -

    WebSvr: Version 0.012

    -

    Lincense: MIT

    +WebSvr: Version 0.012 +============== +Lincense: MIT -

    version: 0.020

    -
      -
    • - Filter: A Request will mapping all the filters first, and then pass to the Handler; -
    • -
    • - Handler: When a request matched a handler, it will returned, so only one handler will be matched; -
    • -
    • - Session: Stored in file, with JSON format; -
    • -
    • - Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ -
    • -
    +version: 0.020 +-------------- +- Filter: A Request will mapping all the filters first, and then pass to the Handler; +- Handler: When a request matched a handler, it will returned, so only one handler will be matched; +- Session: Stored in file, with JSON format; +- Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ -

    version: 0.005

    -
      -
    • - MIME: Suppor mime header, integrate https://github.com/broofa/node-mime -
    • -
    +version: 0.005 +-------------- +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime -

    Sample:

    -
    -  //Start WebSVr, runnting at parent folder, default port is 8054, directory browser enabled;
    -  //Trying at: http://localhost:8054
    -  var webSvr = new WebSvr({root:"./../"});
    -  webSvr.start();
    +Sample:
    +--------------
     
    +    //Start WebSVr, runnting at parent folder, default port is 8054, directory browser enabled;
    +    //Trying at: http://localhost:8054
    +    var webSvr = new WebSvr({root:"./../"});
    +    webSvr.start();
     
    -  var	fs = require("fs"),
    -      querystring = require("querystring");
     
    +    var fs = require("fs"),
    +        querystring = require("querystring");
     
    -  /*
    -  Filter: test/* => (session validation function);
    -    parse:parse the post data and stored to req.body;
    -    session: init the session and stored in req.session; 
    -  */
    -  webSvr.filter(/test\/[\w\.]+/, function(req, res){
    -    //It's not index.htm/login.do, do the session validation
    -    if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){
    -      !req.session.get("username") && res.end("You must login, first!");
    -    }
     
    -    //Link to next filter
    -    req.filter.next(req, res);
    -  }, {parse: true, session: true});
    +    /*
    +    Filter: test/* => (session validation function);
    +      parse:parse the post data and stored to req.body;
    +      session: init the session and stored in req.session; 
    +    */
    +    webSvr.filter(/test\/[\w\.]+/, function(req, res){
    +      //It's not index.htm/login.do, do the session validation
    +      if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){
    +        !req.session.get("username") && res.end("You must login, first!");
    +      }
     
    +      //Link to next filter
    +      req.filter.next(req, res);
    +    }, {parse: true, session: true});
     
    -  /*
    -  Handler: login.do => (validate the username & password)
    -    username: admin
    -    password: 12345678
    -  */
    -  webSvr.url(/login.do/, function(req, res){
    -    //TODO: Add an parameter to auto-complete querystring.parse(req.body);
    -    var qs = querystring.parse(req.body);
    -    if(qs.username == "admin" && qs.password == "12345678"){
    -      //Put key/value pair in session
    -      //TODO: Support put JSON object directly
    -      req.session.set("username", qs.username, function(session){
    -        //TODO: Add req.redirect / req.forward functionalities;
    -        res.writeHead(200, {"Content-Type": "text/html"});
    -        res.writeFile("/test/setting.htm");
    -      });
    -    }else{
    -      res.writeHead(401);
    -      res.end("Wrong username/password");
    -    }
    -  });
     
    +    /*
    +    Handler: login.do => (validate the username & password)
    +      username: admin
    +      password: 12345678
    +    */
    +    webSvr.url(/login.do/, function(req, res){
    +      //TODO: Add an parameter to auto-complete querystring.parse(req.body);
    +      var qs = querystring.parse(req.body);
    +      if(qs.username == "admin" && qs.password == "12345678"){
    +        //Put key/value pair in session
    +        //TODO: Support put JSON object directly
    +        req.session.set("username", qs.username, function(session){
    +          //TODO: Add req.redirect / req.forward functionalities;
    +          res.writeHead(200, {"Content-Type": "text/html"});
    +          res.writeFile("/test/setting.htm");
    +        });
    +      }else{
    +        res.writeHead(401);
    +        res.end("Wrong username/password");
    +      }
    +    });
     
    -  /*
    -  Uploader: upload.do => (receive handler)
    -  */
    -  webSvr.file(/upload.do/, function(req, res){
    -    res.writeHead(200, {"Content-Type": "text/plain"});
    -    //Upload file is stored in req.files
    -    //form fields is stored in req.body
    -    res.write(JSON.stringify(req.body));
    -    res.end(JSON.stringify(req.files));
    -  });
     
    +    /*
    +    Uploader: upload.do => (receive handler)
    +    */
    +    webSvr.file(/upload.do/, function(req, res){
    +      res.writeHead(200, {"Content-Type": "text/plain"});
    +      //Upload file is stored in req.files
    +      //form fields is stored in req.body
    +      res.write(JSON.stringify(req.body));
    +      res.end(JSON.stringify(req.files));
    +    });
     
    -  /*
    -  Simple redirect API:
    -  */
    -  //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine
    -  webSvr.url(/combine/, ["svr/tool/Combine.js"]);
    -  //Mapping "hello" to a string, trying at http://localhost:8054/hello
    -  webSvr.url(/hello/, "Hello WebSvr!");
    -  //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post
    -  webSvr.post(/post/, function(req, res){
    -    res.writeHead(200, {"Content-Type": "text/html"});
    -    //Need session support
    -    res.write("You username is " + req.session.get("username"));
    -    res.write('

    '); - res.end('Received : ' + req.body); - }, {session: true}); -
    \ No newline at end of file + + /* + Simple redirect API: + */ + //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine + webSvr.url(/combine/, ["svr/tool/Combine.js"]); + //Mapping "hello" to a string, trying at http://localhost:8054/hello + webSvr.url(/hello/, "Hello WebSvr!"); + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post + webSvr.post(/post/, function(req, res){ + res.writeHead(200, {"Content-Type": "text/html"}); + //Need session support + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); + }, {session: true}); \ No newline at end of file From 52db5a74aafaffd0e83e29ba39dc44f422bb3587 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 23 Oct 2012 10:08:01 +0800 Subject: [PATCH 010/195] Version: 0.020 Add README --- svr/README => README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename svr/README => README.md (96%) diff --git a/svr/README b/README.md similarity index 96% rename from svr/README rename to README.md index 12fd597..9485d95 100644 --- a/svr/README +++ b/README.md @@ -22,7 +22,7 @@ Sample: webSvr.start(); - var fs = require("fs"), + var fs = require("fs"), querystring = require("querystring"); From a43f3f27d25a67a85a6dd21d8b56894ee4cf42c0 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 23 Oct 2012 10:11:58 +0800 Subject: [PATCH 011/195] Version: 0.020 Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9485d95..bc5a30b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -WebSvr: Version 0.012 +WebSvr ============== -Lincense: MIT +Lincense: MIT, GNU version: 0.020 -------------- @@ -16,7 +16,7 @@ version: 0.005 Sample: -------------- - //Start WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; + //Start WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 var webSvr = new WebSvr({root:"./../"}); webSvr.start(); From df2f30720320325e548d111a1f42f15fc8c04eb2 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 24 Oct 2012 11:48:59 +0800 Subject: [PATCH 012/195] version: 0.022 Add underscore library --- README.md | 43 +- svr/Filter.js | 4 +- svr/Global.js | 8 + svr/Handler.js | 9 +- svr/MakeFile.list | 8 +- svr/SiteTest.js | 14 +- svr/WebSvr.all.js | 60 +- svr/WebSvr.js | 13 +- svr/lib/underscore.js | 1200 +++++++++++++++++ svr/ref/Object.extend.js | 10 - .../session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t | 1 + .../session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J | 1 + 12 files changed, 1272 insertions(+), 99 deletions(-) create mode 100644 svr/Global.js create mode 100644 svr/lib/underscore.js delete mode 100644 svr/ref/Object.extend.js create mode 100644 svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t create mode 100644 svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J diff --git a/README.md b/README.md index bc5a30b..31800e2 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ WebSvr ============== -Lincense: MIT, GNU +Lincense: MIT, GPL -version: 0.020 +Description: -------------- -- Filter: A Request will mapping all the filters first, and then pass to the Handler; +Create a Web Server (http based), all the files will be combined into one, in order to use the various kinds of javascript libraries in Node.js; + + +version: 0.022 +-------------- +- Filter: A request will mapping all the filters first, and then pass to the Handler; - Handler: When a request matched a handler, it will returned, so only one handler will be matched; - Session: Stored in file, with JSON format; -- Form Data: Support upload files, integrate https://github.com/felixge/node-formidable/ +- Form Data: Support upload files, integrate + https://github.com/felixge/node-formidable/ +- Underscore: Add underscore a utility-belt library for JavaScript + https://github.com/documentcloud/underscore version: 0.005 -------------- - MIME: Suppor mime header, integrate https://github.com/broofa/node-mime -Sample: +Start: Edit in SiteTest.js or Create a new Site.js and added to MakeFile.list -------------- //Start WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; @@ -21,11 +29,8 @@ Sample: var webSvr = new WebSvr({root:"./../"}); webSvr.start(); - - var fs = require("fs"), - querystring = require("querystring"); - - +Filter: Session based authentication (session stored in files), all the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" +-------------- /* Filter: test/* => (session validation function); parse:parse the post data and stored to req.body; @@ -41,13 +46,16 @@ Sample: req.filter.next(req, res); }, {parse: true, session: true}); - +Handler: Handle Login and update the username in Session +-------------- /* Handler: login.do => (validate the username & password) username: admin password: 12345678 */ - webSvr.url(/login.do/, function(req, res){ + webSvr.session(/login.do/, function(req, res){ + var querystring = require("querystring"); + //TODO: Add an parameter to auto-complete querystring.parse(req.body); var qs = querystring.parse(req.body); if(qs.username == "admin" && qs.password == "12345678"){ @@ -65,6 +73,8 @@ Sample: }); +File: Receive upload file (it's a specfic filter) +-------------- /* Uploader: upload.do => (receive handler) */ @@ -77,15 +87,14 @@ Sample: }); - /* - Simple redirect API: - */ +Handler: Other simple redirect API +-------------- //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine webSvr.url(/combine/, ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello webSvr.url(/hello/, "Hello WebSvr!"); - //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post - webSvr.post(/post/, function(req, res){ + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm + webSvr.post(/post.htm/, function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); //Need session support res.write("You username is " + req.session.get("username")); diff --git a/svr/Filter.js b/svr/Filter.js index 3d26049..5c3b1e3 100644 --- a/svr/Filter.js +++ b/svr/Filter.js @@ -11,7 +11,7 @@ var Filter = { */ add : function(regExp, handler, options){ var params = {regExp: regExp, handler: handler}; - Filter.filters.push(Object.extend(params, options)); + Filter.filters.push(_.extend(params, options)); }, /* @@ -21,7 +21,7 @@ var Filter = { file: function(regExp, handler, options){ var params = {regExp: regExp, handler: handler, file: true}; //insert as the first elements - Filter.filters.splice(0, 0, Object.extend(params, options)); + Filter.filters.splice(0, 0, _.extend(params, options)); } }; diff --git a/svr/Global.js b/svr/Global.js new file mode 100644 index 0000000..a0554fe --- /dev/null +++ b/svr/Global.js @@ -0,0 +1,8 @@ +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); \ No newline at end of file diff --git a/svr/Handler.js b/svr/Handler.js index b0c696c..19b4511 100644 --- a/svr/Handler.js +++ b/svr/Handler.js @@ -6,11 +6,6 @@ var Handler; (function(){ - /* - Private: web server instance - */ - var webSvr; - /* Private: handler list */ @@ -23,13 +18,13 @@ var Handler; url : function(regExp, handler, options){ var params = {regExp: regExp, handler: handler}; - handlers.push(Object.extend(params, options)); + handlers.push(_.extend(params, options)); }, //Post: Parse the post data by default; post : function(regExp, handler, options){ var params = { parse: true }; - this.url(regExp, handler, Object.extend(params, options)); + this.url(regExp, handler, _.extend(params, options)); }, //Session: Parse the session and post by default; diff --git a/svr/MakeFile.list b/svr/MakeFile.list index d275660..94d6334 100644 --- a/svr/MakeFile.list +++ b/svr/MakeFile.list @@ -1,10 +1,12 @@ -#config file +#Config file #These modules will be crunched to one. #TODO: Need to support dictionary in the list; -#reference +#Global object and description +Global.js + +#Reference ref/Math.uuid.js -ref/Object.extend.js #WebSvr RequestParser.js diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 1aa392c..0b8adf5 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -4,11 +4,6 @@ var webSvr = new WebSvr({root:"./../"}); webSvr.start(); - -var fs = require("fs"), - querystring = require("querystring"); - - /* Filter: test/* => (session validation function); parse:parse the post data and stored to req.body; @@ -30,7 +25,9 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.url(/login.do/, function(req, res){ +webSvr.session(/login.do/, function(req, res){ + var querystring = require("querystring"); + //TODO: Add an parameter to auto-complete querystring.parse(req.body); var qs = querystring.parse(req.body); if(qs.username == "admin" && qs.password == "12345678"){ @@ -47,7 +44,6 @@ webSvr.url(/login.do/, function(req, res){ } }); - /* Uploader: upload.do => (receive handler) */ @@ -67,8 +63,8 @@ Simple redirect API: webSvr.url(/combine/, ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello webSvr.url(/hello/, "Hello WebSvr!"); -//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post -webSvr.post(/post/, function(req, res){ +//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm +webSvr.post(/post.htm/, function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); //Need session support res.write("You username is " + req.session.get("username")); diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index b069139..fd632ff 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -1,3 +1,12 @@ +/*Global.js*/ +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); /*ref\Math.uuid.js*/ /*! Math.uuid.js (v1.4) @@ -92,17 +101,6 @@ Dual licensed under the MIT and GPL licenses. }; })(); -/*ref\Object.extend.js*/ -Object.extend = function(des, src){ - - if(!des) des = {}; - - for(var p in src){ - if(!des[p]) des[p] = src[p]; - } - - return des; -} /*RequestParser.js*/ /* Request parser, parse the data in request body via @@ -305,7 +303,7 @@ var Filter = { */ add : function(regExp, handler, options){ var params = {regExp: regExp, handler: handler}; - Filter.filters.push(Object.extend(params, options)); + Filter.filters.push(_.extend(params, options)); }, /* @@ -315,7 +313,7 @@ var Filter = { file: function(regExp, handler, options){ var params = {regExp: regExp, handler: handler, file: true}; //insert as the first elements - Filter.filters.splice(0, 0, Object.extend(params, options)); + Filter.filters.splice(0, 0, _.extend(params, options)); } }; @@ -363,11 +361,6 @@ var Handler; (function(){ - /* - Private: web server instance - */ - var webSvr; - /* Private: handler list */ @@ -380,13 +373,13 @@ var Handler; url : function(regExp, handler, options){ var params = {regExp: regExp, handler: handler}; - handlers.push(Object.extend(params, options)); + handlers.push(_.extend(params, options)); }, //Post: Parse the post data by default; post : function(regExp, handler, options){ var params = { parse: true }; - this.url(regExp, handler, Object.extend(params, options)); + this.url(regExp, handler, _.extend(params, options)); }, //Session: Parse the session and post by default; @@ -444,16 +437,8 @@ var Handler; /*WebSvr.js*/ /* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, +* Description: Create a Web Server (http based). * Author: Kris Zhang -* Dependence: Node.js: http://www.nodejs.org, -* mime.js: https://github.com/bentomas/node-mime -* Math.uuid.js (v1.4) : http://www.broofa.com -* Date: 2012-3 Draft -* 2012-4 Update: Using async and mime.js -* 2012-7 Update: Rename and reformat files */ /* * WebSvr Namespace @@ -656,9 +641,8 @@ var WebSvr = (function(){ //Public: start http server self.start = function(){ - options = options || {}; - Object.extend(options, defaults); + options = _.extend(defaults, options); root = options.root; port = parseInt(options.port); @@ -707,11 +691,6 @@ var WebSvr = (function(){ var webSvr = new WebSvr({root:"./../"}); webSvr.start(); - -var fs = require("fs"), - querystring = require("querystring"); - - /* Filter: test/* => (session validation function); parse:parse the post data and stored to req.body; @@ -733,7 +712,9 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.url(/login.do/, function(req, res){ +webSvr.session(/login.do/, function(req, res){ + var querystring = require("querystring"); + //TODO: Add an parameter to auto-complete querystring.parse(req.body); var qs = querystring.parse(req.body); if(qs.username == "admin" && qs.password == "12345678"){ @@ -750,7 +731,6 @@ webSvr.url(/login.do/, function(req, res){ } }); - /* Uploader: upload.do => (receive handler) */ @@ -770,8 +750,8 @@ Simple redirect API: webSvr.url(/combine/, ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello webSvr.url(/hello/, "Hello WebSvr!"); -//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post -webSvr.post(/post/, function(req, res){ +//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm +webSvr.post(/post.htm/, function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); //Need session support res.write("You username is " + req.session.get("username")); diff --git a/svr/WebSvr.js b/svr/WebSvr.js index feb49c7..4b5915b 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -1,14 +1,6 @@ /* -* Description: Create a static file server (http based). -* This will list all the files and directories via Node.Js. -* The behavior will be like directory browsing enabled in IIS, +* Description: Create a Web Server (http based). * Author: Kris Zhang -* Dependence: Node.js: http://www.nodejs.org, -* mime.js: https://github.com/bentomas/node-mime -* Math.uuid.js (v1.4) : http://www.broofa.com -* Date: 2012-3 Draft -* 2012-4 Update: Using async and mime.js -* 2012-7 Update: Rename and reformat files */ /* * WebSvr Namespace @@ -211,9 +203,8 @@ var WebSvr = (function(){ //Public: start http server self.start = function(){ - options = options || {}; - Object.extend(options, defaults); + options = _.extend(defaults, options); root = options.root; port = parseInt(options.port); diff --git a/svr/lib/underscore.js b/svr/lib/underscore.js new file mode 100644 index 0000000..1ebe267 --- /dev/null +++ b/svr/lib/underscore.js @@ -0,0 +1,1200 @@ +// Underscore.js 1.4.2 +// http://underscorejs.org +// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root['_'] = _; + } + + // Current version. + _.VERSION = '1.4.2'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + each(obj, function(value, index, list) { + if (!iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + found = any(obj, function(value) { + return value === target; + }); + return found; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + return _.map(obj, function(value) { + return (_.isFunction(method) ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // with specific `key:value` pairs. + _.where = function(obj, attrs) { + if (_.isEmpty(attrs)) return []; + return _.filter(obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + index : index, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index < right.index ? -1 : 1; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(obj, value, context, behavior) { + var result = {}; + var iterator = lookupIterator(value); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + }; + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + if (!_.has(result, key)) result[key] = 0; + result[key]++; + }); + }; + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(obj) { + if (!obj) return []; + if (obj.length === +obj.length) return slice.call(obj); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, function(value){ return !!value; }); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + each(input, function(value) { + if (_.isArray(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(concat.apply(ArrayProto, arguments)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(args, "" + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + var result = {}; + for (var i = 0, l = list.length; i < l; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, l = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. + // Delegates to **ECMAScript 5**'s native `Function.bind` if available. + // We check for `func.bind` first, to fail fast when `func` is undefined. + _.bind = function bind(func, context) { + var bound, args; + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length == 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, throttling, more, result; + var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + return function() { + context = this; args = arguments; + var later = function() { + timeout = null; + if (more) { + result = func.apply(context, args); + } + whenDone(); + }; + if (!timeout) timeout = setTimeout(later, wait); + if (throttling) { + more = true; + } else { + throttling = true; + result = func.apply(context, args); + } + whenDone(); + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var values = []; + for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var pairs = []; + for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return _.isNumber(obj) && isFinite(obj); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + for (var i = 0; i < n; i++) iterator.call(context, i); + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + (0 | Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named property is a function then invoke it; + // otherwise, return it. + _.result = function(object, property) { + if (object == null) return null; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = idCounter++; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + source += + escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : + interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : + evaluate ? "';\n" + evaluate + "\n__p+='" : ''; + index = offset + match.length; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); diff --git a/svr/ref/Object.extend.js b/svr/ref/Object.extend.js deleted file mode 100644 index 13254a8..0000000 --- a/svr/ref/Object.extend.js +++ /dev/null @@ -1,10 +0,0 @@ -Object.extend = function(des, src){ - - if(!des) des = {}; - - for(var p in src){ - if(!des[p]) des[p] = src[p]; - } - - return des; -} \ No newline at end of file diff --git a/svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t b/svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t new file mode 100644 index 0000000..42adc36 --- /dev/null +++ b/svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t @@ -0,0 +1 @@ +{"username":"admin"} \ No newline at end of file diff --git a/svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J b/svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J new file mode 100644 index 0000000..42adc36 --- /dev/null +++ b/svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J @@ -0,0 +1 @@ +{"username":"admin"} \ No newline at end of file From 847dafc6f76f3d0ffd24326b5daecfc726694236 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 24 Oct 2012 14:13:00 +0800 Subject: [PATCH 013/195] version: 0.020 Update comments --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31800e2..2b9d1ec 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Description: Create a Web Server (http based), all the files will be combined into one, in order to use the various kinds of javascript libraries in Node.js; -version: 0.022 +Version: 0.021 -------------- - Filter: A request will mapping all the filters first, and then pass to the Handler; - Handler: When a request matched a handler, it will returned, so only one handler will be matched; @@ -17,7 +17,7 @@ version: 0.022 - Underscore: Add underscore a utility-belt library for JavaScript https://github.com/documentcloud/underscore -version: 0.005 +Version: 0.005 -------------- - MIME: Suppor mime header, integrate https://github.com/broofa/node-mime From 8a0c0fe71a8a61a5c3a5be4dfe87778feba58b44 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 26 Oct 2012 13:22:53 +0800 Subject: [PATCH 014/195] move folders move temporary folder out of svr; --- tmp/file/readme.txt | 1 + tmp/session/readme.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 tmp/file/readme.txt create mode 100644 tmp/session/readme.txt diff --git a/tmp/file/readme.txt b/tmp/file/readme.txt new file mode 100644 index 0000000..9cf689f --- /dev/null +++ b/tmp/file/readme.txt @@ -0,0 +1 @@ +Temporary upload files here. \ No newline at end of file diff --git a/tmp/session/readme.txt b/tmp/session/readme.txt new file mode 100644 index 0000000..0b4b98a --- /dev/null +++ b/tmp/session/readme.txt @@ -0,0 +1 @@ +Temporary session here. \ No newline at end of file From e358ad625dc08898178445d4498f4c0b0a5d6122 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 26 Oct 2012 15:04:26 +0800 Subject: [PATCH 015/195] version 0.022 add https support add setting file --- svr/MakeFile.list | 5 +- svr/Parser.js | 6 +- svr/SessionParser.js | 6 +- svr/Settings.js | 25 +++++++ svr/SiteTest.js | 2 +- svr/WebSvr.all.js | 66 +++++++++++++++---- svr/WebSvr.js | 25 +++++-- .../session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t | 1 - .../session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r | 1 - .../session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J | 1 - tmp/{file => upload}/readme.txt | 0 11 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 svr/Settings.js delete mode 100644 svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t delete mode 100644 svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r delete mode 100644 svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J rename tmp/{file => upload}/readme.txt (100%) diff --git a/svr/MakeFile.list b/svr/MakeFile.list index 94d6334..36befa5 100644 --- a/svr/MakeFile.list +++ b/svr/MakeFile.list @@ -4,11 +4,12 @@ #Global object and description Global.js +Settings.js -#Reference +#Reference: JavaScript library ref/Math.uuid.js -#WebSvr +#WebSvr Modules RequestParser.js SessionParser.js Parser.js diff --git a/svr/Parser.js b/svr/Parser.js index 844f148..2a6d7a6 100644 --- a/svr/Parser.js +++ b/svr/Parser.js @@ -42,9 +42,11 @@ var Parser = function(req, res, mapper){ //Need to parse the file in request? if(mapper.file && typeof req.body == "undefined"){ //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'); + var formidable = require('./lib/incoming_form'), + form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; - var form = new formidable.IncomingForm(); form.parse(req, function(err, fields, files) { if (err){ console.log(err); diff --git a/svr/SessionParser.js b/svr/SessionParser.js index 3635b30..7baf511 100644 --- a/svr/SessionParser.js +++ b/svr/SessionParser.js @@ -8,6 +8,8 @@ var SessionParser; SessionParser = (function(req, res, callback){ + var sessionDir = Settings.sessionDir; + var self = { //session id sid : null, @@ -18,7 +20,7 @@ var SessionParser; //TODO self.set = function(key, val, callback){ - var sessionfile = 'tmp/session/' + self.sid; + var sessionfile = sessionDir + self.sid; key && (self.obj[key] = val); @@ -56,7 +58,7 @@ var SessionParser; self.sid = sidVal; //We only receive the cookie from Http headers - var sessionfile = 'tmp/session/' + self.sid; + var sessionfile = sessionDir + self.sid; //here will be cause a bit of delay fs.exists(sessionfile, function (exists) { diff --git a/svr/Settings.js b/svr/Settings.js new file mode 100644 index 0000000..a6a0264 --- /dev/null +++ b/svr/Settings.js @@ -0,0 +1,25 @@ +/* +Configurations +*/ +var Settings = { + version: 0.022, + + //root folder of web + root: "../", + + //default port of web + port: 8054, + //enable https? + https: { + enable: false, + port: 8443, + options: { + //pfx: require("fs").readFileSync('server.pfx') + } + }, + + //session file stored here, must be end with "/" + sessionDir: "../tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "../tmp/upload/" +}; diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 0b8adf5..87bc841 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -1,7 +1,7 @@ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({root:"./../"}); +var webSvr = new WebSvr(); webSvr.start(); /* diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index fd632ff..394cd2c 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -7,6 +7,33 @@ //Underscore global object; var _ = require("./lib/underscore"); +/*Settings.js*/ +/* +Configurations +*/ +var Settings = { + version: 0.022, + + //root folder of web + root: "../", + + //default port of web + port: 8054, + //enable https? + https: { + enable: false, + port: 8443, + options: { + //pfx: require("fs").readFileSync('server.pfx') + } + }, + + //session file stored here, must be end with "/" + sessionDir: "../tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "../tmp/upload/" +}; + /*ref\Math.uuid.js*/ /*! Math.uuid.js (v1.4) @@ -140,6 +167,8 @@ var SessionParser; SessionParser = (function(req, res, callback){ + var sessionDir = Settings.sessionDir; + var self = { //session id sid : null, @@ -150,7 +179,7 @@ var SessionParser; //TODO self.set = function(key, val, callback){ - var sessionfile = 'tmp/session/' + self.sid; + var sessionfile = sessionDir + self.sid; key && (self.obj[key] = val); @@ -188,7 +217,7 @@ var SessionParser; self.sid = sidVal; //We only receive the cookie from Http headers - var sessionfile = 'tmp/session/' + self.sid; + var sessionfile = sessionDir + self.sid; //here will be cause a bit of delay fs.exists(sessionfile, function (exists) { @@ -264,9 +293,11 @@ var Parser = function(req, res, mapper){ //Need to parse the file in request? if(mapper.file && typeof req.body == "undefined"){ //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'); + var formidable = require('./lib/incoming_form'), + form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; - var form = new formidable.IncomingForm(); form.parse(req, function(err, fields, files) { if (err){ console.log(err); @@ -641,27 +672,40 @@ var WebSvr = (function(){ //Public: start http server self.start = function(){ - - options = _.extend(defaults, options); + //Update the default value of Settings + options = _.extend(Settings, options); root = options.root; port = parseInt(options.port); - //Expose the API - self.options = options; - try{ //Create http server var httpSvr = require("http").createServer(requestHandler); httpSvr.listen(port); - console.log("Running at" + console.log("Http server running at" ,"Root:", root ,"Port:", port ); self.httpSvr = httpSvr; + //Create https server + if(options.https){ + var httpsOptions = options.https.options, + httpsPort = options.https.port; + + var httpsSvr = require("https").createServer(httpsOptions, requestHandler); + httpsSvr.listen(httpsPort); + + console.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); + + self.httpsSvr = httpsSvr; + } + return true; } catch(err){ @@ -688,7 +732,7 @@ var WebSvr = (function(){ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({root:"./../"}); +var webSvr = new WebSvr(); webSvr.start(); /* diff --git a/svr/WebSvr.js b/svr/WebSvr.js index 4b5915b..dffb72b 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -203,27 +203,40 @@ var WebSvr = (function(){ //Public: start http server self.start = function(){ - - options = _.extend(defaults, options); + //Update the default value of Settings + options = _.extend(Settings, options); root = options.root; port = parseInt(options.port); - //Expose the API - self.options = options; - try{ //Create http server var httpSvr = require("http").createServer(requestHandler); httpSvr.listen(port); - console.log("Running at" + console.log("Http server running at" ,"Root:", root ,"Port:", port ); self.httpSvr = httpSvr; + //Create https server + if(options.https){ + var httpsOptions = options.https.options, + httpsPort = options.https.port; + + var httpsSvr = require("https").createServer(httpsOptions, requestHandler); + httpsSvr.listen(httpsPort); + + console.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); + + self.httpsSvr = httpsSvr; + } + return true; } catch(err){ diff --git a/svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t b/svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t deleted file mode 100644 index 42adc36..0000000 --- a/svr/tmp/session/XqKyassLKzvplKYQhpZZcN9c0zHqGJ5t +++ /dev/null @@ -1 +0,0 @@ -{"username":"admin"} \ No newline at end of file diff --git a/svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r b/svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r deleted file mode 100644 index 42adc36..0000000 --- a/svr/tmp/session/ZKfm7vTA5PkHUqTz3rmodRsmZymbEv0r +++ /dev/null @@ -1 +0,0 @@ -{"username":"admin"} \ No newline at end of file diff --git a/svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J b/svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J deleted file mode 100644 index 42adc36..0000000 --- a/svr/tmp/session/qpPELadrOz60RxOMS89Clbm7wxFU8l3J +++ /dev/null @@ -1 +0,0 @@ -{"username":"admin"} \ No newline at end of file diff --git a/tmp/file/readme.txt b/tmp/upload/readme.txt similarity index 100% rename from tmp/file/readme.txt rename to tmp/upload/readme.txt From 35b028cb8506658b631c162654e58ee5c70fc8f0 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 28 Oct 2012 16:43:47 +0800 Subject: [PATCH 016/195] https enable SSL: support --- svr/Settings.js | 19 ++++++------ svr/SiteTest.js | 6 ++-- svr/WebSvr.all.js | 62 +++++++++++++++++++--------------------- svr/WebSvr.js | 37 +++++++++++------------- svr/cert/certificate.pem | 17 +++++++++++ svr/cert/privatekey.pem | 15 ++++++++++ 6 files changed, 92 insertions(+), 64 deletions(-) create mode 100644 svr/cert/certificate.pem create mode 100644 svr/cert/privatekey.pem diff --git a/svr/Settings.js b/svr/Settings.js index a6a0264..2da4c87 100644 --- a/svr/Settings.js +++ b/svr/Settings.js @@ -7,15 +7,18 @@ var Settings = { //root folder of web root: "../", - //default port of web + //http + http: true, + //default port of http port: 8054, - //enable https? - https: { - enable: false, - port: 8443, - options: { - //pfx: require("fs").readFileSync('server.pfx') - } + + //https + https: false, + //default port of https + httpsPort: 8443, + httpsOpts: { + key: require("fs").readFileSync("cert/privatekey.pem"), + cert: require("fs").readFileSync("cert/certificate.pem") }, //session file stored here, must be end with "/" diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 87bc841..68a5f69 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -1,12 +1,12 @@ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr(); +var webSvr = new WebSvr({https:true, httpsPort:443}); webSvr.start(); /* Filter: test/* => (session validation function); - parse:parse the post data and stored to req.body; + parse:parse the post data and stored in req.body; session: init the session and stored in req.session; */ webSvr.filter(/test\/[\w\.]+/, function(req, res){ @@ -66,7 +66,7 @@ webSvr.url(/hello/, "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm webSvr.post(/post.htm/, function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); - //Need session support + //Witch session support: "{session: true}" res.write("You username is " + req.session.get("username")); res.write('

    '); res.end('Received : ' + req.body); diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index 394cd2c..615d599 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -17,15 +17,18 @@ var Settings = { //root folder of web root: "../", - //default port of web + //http + http: true, + //default port of http port: 8054, - //enable https? - https: { - enable: false, - port: 8443, - options: { - //pfx: require("fs").readFileSync('server.pfx') - } + + //https + https: false, + //default port of https + httpsPort: 8443, + httpsOpts: { + key: require("fs").readFileSync("cert/privatekey.pem"), + cert: require("fs").readFileSync("cert/certificate.pem") }, //session file stored here, must be end with "/" @@ -673,13 +676,13 @@ var WebSvr = (function(){ //Public: start http server self.start = function(){ //Update the default value of Settings - options = _.extend(Settings, options); + options = _.extend({}, Settings, options); root = options.root; port = parseInt(options.port); - try{ - //Create http server + //Create http server + if(options.http){ var httpSvr = require("http").createServer(requestHandler); httpSvr.listen(port); @@ -689,29 +692,24 @@ var WebSvr = (function(){ ); self.httpSvr = httpSvr; + } - //Create https server - if(options.https){ - var httpsOptions = options.https.options, - httpsPort = options.https.port; - - var httpsSvr = require("https").createServer(httpsOptions, requestHandler); - httpsSvr.listen(httpsPort); + //Create https server + if(options.https){ + var httpsOpts = options.httpsOpts, + httpsPort = options.httpsPort; - console.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); + var httpsSvr = require("https").createServer(httpsOpts, requestHandler); + httpsSvr.listen(httpsPort); - self.httpsSvr = httpsSvr; - } + console.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); - return true; - } - catch(err){ - console.log("Can't setup server at port", port, err); + self.httpsSvr = httpsSvr; } - return false; + }; //Public: close http server; @@ -732,12 +730,12 @@ var WebSvr = (function(){ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr(); +var webSvr = new WebSvr({https:true, httpsPort:443}); webSvr.start(); /* Filter: test/* => (session validation function); - parse:parse the post data and stored to req.body; + parse:parse the post data and stored in req.body; session: init the session and stored in req.session; */ webSvr.filter(/test\/[\w\.]+/, function(req, res){ @@ -797,7 +795,7 @@ webSvr.url(/hello/, "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm webSvr.post(/post.htm/, function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); - //Need session support + //Witch session support: "{session: true}" res.write("You username is " + req.session.get("username")); res.write('

    '); res.end('Received : ' + req.body); diff --git a/svr/WebSvr.js b/svr/WebSvr.js index dffb72b..ba58fd3 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -204,13 +204,13 @@ var WebSvr = (function(){ //Public: start http server self.start = function(){ //Update the default value of Settings - options = _.extend(Settings, options); + options = _.extend({}, Settings, options); root = options.root; port = parseInt(options.port); - try{ - //Create http server + //Create http server + if(options.http){ var httpSvr = require("http").createServer(requestHandler); httpSvr.listen(port); @@ -220,29 +220,24 @@ var WebSvr = (function(){ ); self.httpSvr = httpSvr; + } - //Create https server - if(options.https){ - var httpsOptions = options.https.options, - httpsPort = options.https.port; - - var httpsSvr = require("https").createServer(httpsOptions, requestHandler); - httpsSvr.listen(httpsPort); + //Create https server + if(options.https){ + var httpsOpts = options.httpsOpts, + httpsPort = options.httpsPort; - console.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); + var httpsSvr = require("https").createServer(httpsOpts, requestHandler); + httpsSvr.listen(httpsPort); - self.httpsSvr = httpsSvr; - } + console.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); - return true; - } - catch(err){ - console.log("Can't setup server at port", port, err); + self.httpsSvr = httpsSvr; } - return false; + }; //Public: close http server; diff --git a/svr/cert/certificate.pem b/svr/cert/certificate.pem new file mode 100644 index 0000000..fa2f72c --- /dev/null +++ b/svr/cert/certificate.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICpTCCAg4CCQCISrlY6+bq+DANBgkqhkiG9w0BAQUFADCBljELMAkGA1UEBhMC +Q04xETAPBgNVBAgMCFNoYW5naGFpMREwDwYDVQQHDAhTaGFuZ2hhaTEQMA4GA1UE +CgwHRW5sb2dpYzEfMB0GA1UECwwWUmVzZWFyY2ggJiBEZXZlbG9wbWVudDESMBAG +A1UEAwwJbG9jYWxob3N0MRowGAYJKoZIhvcNAQkBFgtjMnVAbGl2ZS5jbjAeFw0x +MjEwMjgwNzU1MTZaFw0xMjExMjcwNzU1MTZaMIGWMQswCQYDVQQGEwJDTjERMA8G +A1UECAwIU2hhbmdoYWkxETAPBgNVBAcMCFNoYW5naGFpMRAwDgYDVQQKDAdFbmxv +Z2ljMR8wHQYDVQQLDBZSZXNlYXJjaCAmIERldmVsb3BtZW50MRIwEAYDVQQDDAls +b2NhbGhvc3QxGjAYBgkqhkiG9w0BCQEWC2MydUBsaXZlLmNuMIGfMA0GCSqGSIb3 +DQEBAQUAA4GNADCBiQKBgQCgWDgMTdIEafBqczxi4BpFOL741bf3nttkJxS5eMbj +bQvfDX9LHkNpqZFx3jDWecmjBhUY+B2Q1PmiBnwuRLIBaYsg3jT2nWp0fphzBcJ5 +JsPF3S9GjeWrzYZIuK+YRyCarlmzbjn4omidm/4wAtXuXxQYiouTMe9hyuLnxb6s +mQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAB/dirZrcdB4aGUTlSGWt+LmFuz/Xl6x +iWkfjwvQP7h4oCGoSUjoB9vqy5hIkCKxI+PWhW76S7YpJNXHIdjrC7BRyYVIlAmp +Efm4GUlBAdSYo222my1xTF8SaEx1debDfzq2Ho8xeozpnKae4x62MoA+Fp2kphsq +tLTB8YxbjIQy +-----END CERTIFICATE----- diff --git a/svr/cert/privatekey.pem b/svr/cert/privatekey.pem new file mode 100644 index 0000000..446cf0b --- /dev/null +++ b/svr/cert/privatekey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCgWDgMTdIEafBqczxi4BpFOL741bf3nttkJxS5eMbjbQvfDX9L +HkNpqZFx3jDWecmjBhUY+B2Q1PmiBnwuRLIBaYsg3jT2nWp0fphzBcJ5JsPF3S9G +jeWrzYZIuK+YRyCarlmzbjn4omidm/4wAtXuXxQYiouTMe9hyuLnxb6smQIDAQAB +AoGAWDpSlMqZPh6A0EIaPxmqut4PjuIiORlrBL/QUoHXhjpxZsmJem7rjw9j3XDy +FIGs5owpPbUAp7nYpkPFPrxD6U3qVwZk/C7K2Lx2vWYlJ9vf8TTWAvIt6yyNuq5x +ocFosz346viky3K5QkyvmT9y3UuQS65GKj8Hv5S9xSolAhUCQQDSGzabd9c1kgex +EH96Fu/EWIE2G2U3f5/MRalAPlCIrVkBo1YjNoO8QDIb61mW0KdunQrtxrOoKDJp +Opxwpkj7AkEAw15qkHWd2yfU4yw7cFWsWOLojs3pyLdSX+7++vT3gLL8y2otbyKi +mWlrQJDgCnbXywUyRJtMGmLrn9X0y5IUewJBAJxGlYVpy+8SoRn4dXjwGoLmeaUv +F0gCa29a2RrpvqkKlst7HBSw9adN8HeHxGlC5WaG9JwLUZHf5C8U40t+w4UCQClz +keaeneSO2fNtQhs+gjfFxRPviofEpZynJ8B1U0IiN9Ks74Dh91/XZyMm2fI+buCr +dJPr40TB8j5SdgLvNpsCQFaYxoFde72QZHBQKYMsOmKldhcSuQKeZ/oppuhjgYqp +9qrtIyQ2MTchc8Yu27WCU975F0UGWQTsNG0BqVtGLik= +-----END RSA PRIVATE KEY----- From c2ae5a69421f5586e22e2526a5df4203d7d6d08c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 31 Oct 2012 14:35:53 +0800 Subject: [PATCH 017/195] Mapper: Added Added for Filter & Handler, it will support string in match expression, which only support regular expression before. --- README.md | 5 +- svr/Filter.js | 18 +++--- svr/Handler.js | 71 +++++++++++------------- svr/MakeFile.list | 1 + svr/Mapper.js | 46 ++++++++++++++++ svr/SiteTest.js | 2 +- svr/WebSvr.all.js | 138 ++++++++++++++++++++++++++++++---------------- 7 files changed, 183 insertions(+), 98 deletions(-) create mode 100644 svr/Mapper.js diff --git a/README.md b/README.md index 2b9d1ec..f8389b9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Description: Create a Web Server (http based), all the files will be combined into one, in order to use the various kinds of javascript libraries in Node.js; -Version: 0.021 +Version: 0.024 -------------- - Filter: A request will mapping all the filters first, and then pass to the Handler; - Handler: When a request matched a handler, it will returned, so only one handler will be matched; @@ -16,9 +16,6 @@ Version: 0.021 https://github.com/felixge/node-formidable/ - Underscore: Add underscore a utility-belt library for JavaScript https://github.com/documentcloud/underscore - -Version: 0.005 --------------- - MIME: Suppor mime header, integrate https://github.com/broofa/node-mime Start: Edit in SiteTest.js or Create a new Site.js and added to MakeFile.list diff --git a/svr/Filter.js b/svr/Filter.js index 5c3b1e3..02670fb 100644 --- a/svr/Filter.js +++ b/svr/Filter.js @@ -9,19 +9,19 @@ var Filter = { /* add a new filter */ - add : function(regExp, handler, options){ - var params = {regExp: regExp, handler: handler}; - Filter.filters.push(_.extend(params, options)); + add : function(expression, handler, options){ + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); }, /* file receiver: it's a specfic filter, this filter should be always at the top of the filter list */ - file: function(regExp, handler, options){ - var params = {regExp: regExp, handler: handler, file: true}; + file: function(expression, handler, options){ + var mapper = new Mapper(expression, handler, {file: true}); //insert as the first elements - Filter.filters.splice(0, 0, _.extend(params, options)); + Filter.filters.splice(0, 0, mapper); } }; @@ -52,8 +52,10 @@ FilterChain.prototype = { req.next(req, res); }, options); */ - if(mapper.regExp && mapper.regExp.test(req.url)){ - console.log("filter matched", self.idx, mapper.regExp, req.url); + if(mapper.match(req)){ + + console.log("filter matched", self.idx, mapper.expression, req.url); + Parser(req, res, mapper); }else{ self.next(req, res); diff --git a/svr/Handler.js b/svr/Handler.js index 19b4511..89f8665 100644 --- a/svr/Handler.js +++ b/svr/Handler.js @@ -16,20 +16,19 @@ var Handler; */ Handler = { - url : function(regExp, handler, options){ - var params = {regExp: regExp, handler: handler}; - handlers.push(_.extend(params, options)); + url : function(expression, handler, options){ + var mapper = new Mapper(expression, handler, options); + handlers.push(mapper); }, //Post: Parse the post data by default; - post : function(regExp, handler, options){ - var params = { parse: true }; - this.url(regExp, handler, _.extend(params, options)); + post : function(expression, handler, options){ + this.url(expression, handler, _.extend({ parse: true }, options)); }, //Session: Parse the session and post by default; - session : function(regExp, handler){ - this.url(regExp, handler, { parse:true, session: true }); + session : function(expression, handler){ + this.url(expression, handler, { parse: true, session: true }); }, handle : function(req, res){ @@ -37,41 +36,37 @@ var Handler; for(var i = 0, len = handlers.length; i < len ; i++){ var mapper = handlers[i]; - if(mapper.regExp && mapper.regExp.test(req.url)){ - - console.log("handler matched", i, mapper.regExp, req.url); - - try{ - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type){ - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - } - catch(err){ - console.log(err) - } + if(mapper.match(req)){ + + console.log("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type){ + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } return true; } } + return false; - } //end of handler + + } //end of handle }; diff --git a/svr/MakeFile.list b/svr/MakeFile.list index 36befa5..bba8bb0 100644 --- a/svr/MakeFile.list +++ b/svr/MakeFile.list @@ -10,6 +10,7 @@ Settings.js ref/Math.uuid.js #WebSvr Modules +Mapper.js RequestParser.js SessionParser.js Parser.js diff --git a/svr/Mapper.js b/svr/Mapper.js new file mode 100644 index 0000000..e420874 --- /dev/null +++ b/svr/Mapper.js @@ -0,0 +1,46 @@ +/* +Mapper: Used for Filter & Handler, +expression: required parameter +handler: required parameter +options: other optional parameters +*/ + +var Mapper = function(expression, handler, options){ + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); +}; + +Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req){ + var self = this, + expression = self.expression; + + switch(expression.constructor){ + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options){ + for(key in options){ + this[key] = options[key] + } + } +}; \ No newline at end of file diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 68a5f69..91e3542 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -1,7 +1,7 @@ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({https:true, httpsPort:443}); +var webSvr = new WebSvr({https:true, httpsPort:8443}); webSvr.start(); /* diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index 615d599..b22094a 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -131,6 +131,53 @@ Dual licensed under the MIT and GPL licenses. }; })(); +/*Mapper.js*/ +/* +Mapper: Used for Filter & Handler, +expression: required parameter +handler: required parameter +options: other optional parameters +*/ + +var Mapper = function(expression, handler, options){ + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); +}; + +Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req){ + var self = this, + expression = self.expression; + + switch(expression.constructor){ + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options){ + for(key in options){ + this[key] = options[key] + } + } +}; /*RequestParser.js*/ /* Request parser, parse the data in request body via @@ -335,19 +382,19 @@ var Filter = { /* add a new filter */ - add : function(regExp, handler, options){ - var params = {regExp: regExp, handler: handler}; - Filter.filters.push(_.extend(params, options)); + add : function(expression, handler, options){ + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); }, /* file receiver: it's a specfic filter, this filter should be always at the top of the filter list */ - file: function(regExp, handler, options){ - var params = {regExp: regExp, handler: handler, file: true}; + file: function(expression, handler, options){ + var mapper = new Mapper(expression, handler, {file: true}); //insert as the first elements - Filter.filters.splice(0, 0, _.extend(params, options)); + Filter.filters.splice(0, 0, mapper); } }; @@ -378,8 +425,10 @@ FilterChain.prototype = { req.next(req, res); }, options); */ - if(mapper.regExp && mapper.regExp.test(req.url)){ - console.log("filter matched", self.idx, mapper.regExp, req.url); + if(mapper.match(req)){ + + console.log("filter matched", self.idx, mapper.expression, req.url); + Parser(req, res, mapper); }else{ self.next(req, res); @@ -405,20 +454,19 @@ var Handler; */ Handler = { - url : function(regExp, handler, options){ - var params = {regExp: regExp, handler: handler}; - handlers.push(_.extend(params, options)); + url : function(expression, handler, options){ + var mapper = new Mapper(expression, handler, options); + handlers.push(mapper); }, //Post: Parse the post data by default; - post : function(regExp, handler, options){ - var params = { parse: true }; - this.url(regExp, handler, _.extend(params, options)); + post : function(expression, handler, options){ + this.url(expression, handler, _.extend({ parse: true }, options)); }, //Session: Parse the session and post by default; - session : function(regExp, handler){ - this.url(regExp, handler, { parse:true, session: true }); + session : function(expression, handler){ + this.url(expression, handler, { parse: true, session: true }); }, handle : function(req, res){ @@ -426,41 +474,37 @@ var Handler; for(var i = 0, len = handlers.length; i < len ; i++){ var mapper = handlers[i]; - if(mapper.regExp && mapper.regExp.test(req.url)){ - - console.log("handler matched", i, mapper.regExp, req.url); - - try{ - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type){ - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - } - catch(err){ - console.log(err) - } + if(mapper.match(req)){ + + console.log("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type){ + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } return true; } } + return false; - } //end of handler + + } //end of handle }; @@ -730,7 +774,7 @@ var WebSvr = (function(){ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({https:true, httpsPort:443}); +var webSvr = new WebSvr({https:true, httpsPort:8443}); webSvr.start(); /* From f9f8c1e0e872cad5ecb23218b96dafa4b0876641 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 31 Oct 2012 21:04:41 +0800 Subject: [PATCH 018/195] Filter: make expression optional i.e: no expression means this filter will apply on all request webSvr.filter([expression, ] function(){ //To do sth. } [, options] ); --- svr/Filter.js | 14 ++- svr/Handler.js | 7 +- svr/Mapper.js | 3 + svr/SiteTest.js | 17 +++- svr/WebSvr.all.js | 245 ++++++++++++++++++++++++++-------------------- svr/WebSvr.js | 6 +- 6 files changed, 175 insertions(+), 117 deletions(-) diff --git a/svr/Filter.js b/svr/Filter.js index 02670fb..2057b97 100644 --- a/svr/Filter.js +++ b/svr/Filter.js @@ -7,9 +7,19 @@ var Filter = { filters : [], /* - add a new filter + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] */ - add : function(expression, handler, options){ + filter : function(expression, handler, options){ + //The first parameter is Function => (handler, options) + if(expression.constructor == Function){ + options = handler; + handler = expression; + expression = null; + } + var mapper = new Mapper(expression, handler, options); Filter.filters.push(mapper); }, diff --git a/svr/Handler.js b/svr/Handler.js index 89f8665..6be15c9 100644 --- a/svr/Handler.js +++ b/svr/Handler.js @@ -15,7 +15,12 @@ var Handler; Static Handler instance */ Handler = { - + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ url : function(expression, handler, options){ var mapper = new Mapper(expression, handler, options); handlers.push(mapper); diff --git a/svr/Mapper.js b/svr/Mapper.js index e420874..f194dc0 100644 --- a/svr/Mapper.js +++ b/svr/Mapper.js @@ -23,6 +23,9 @@ Mapper.prototype = { var self = this, expression = self.expression; + //No expression? It's a general filter mapper + if(!expression) return true; + switch(expression.constructor){ case String: return req.url.indexOf(expression) > -1; case RegExp: return expression.test(req.url); diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 91e3542..71de9bf 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -5,10 +5,21 @@ var webSvr = new WebSvr({https:true, httpsPort:8443}); webSvr.start(); /* -Filter: test/* => (session validation function); - parse:parse the post data and stored in req.body; +General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ +webSvr.filter(function(req, res){ + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(req, res); +}, {parse:true, session:true}); + +/* +Session Filter: protect test/* folder => (validation by session); +*/ webSvr.filter(/test\/[\w\.]+/, function(req, res){ //It's not index.htm/login.do, do the session validation if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ @@ -17,7 +28,7 @@ webSvr.filter(/test\/[\w\.]+/, function(req, res){ //Link to next filter req.filter.next(req, res); -}, {parse: true, session: true}); +}); /* diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index b22094a..fccf645 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -1,11 +1,11 @@ /*Global.js*/ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang -*/ - -//Underscore global object; +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +*/ + +//Underscore global object; var _ = require("./lib/underscore"); /*Settings.js*/ /* @@ -38,98 +38,98 @@ var Settings = { }; /*ref\Math.uuid.js*/ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); /*Mapper.js*/ /* @@ -157,6 +157,9 @@ Mapper.prototype = { var self = this, expression = self.expression; + //No expression? It's a general filter mapper + if(!expression) return true; + switch(expression.constructor){ case String: return req.url.indexOf(expression) > -1; case RegExp: return expression.test(req.url); @@ -380,9 +383,19 @@ var Filter = { filters : [], /* - add a new filter + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] */ - add : function(expression, handler, options){ + filter : function(expression, handler, options){ + //The first parameter is Function => (handler, options) + if(expression.constructor == Function){ + options = handler; + handler = expression; + expression = null; + } + var mapper = new Mapper(expression, handler, options); Filter.filters.push(mapper); }, @@ -453,7 +466,12 @@ var Handler; Static Handler instance */ Handler = { - + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ url : function(expression, handler, options){ var mapper = new Mapper(expression, handler, options); handlers.push(mapper); @@ -650,11 +668,11 @@ var WebSvr = (function(){ //Rewrite the write/writeHead functionalities of current response object var endFn = res.end; res.end = function(){ - //Execute old end; + //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object res.write = res.writeHead = function(){ - console.log("response is already end, response write ignored!") + console.log("response is already end, response.write ignored!") }; }; @@ -685,7 +703,7 @@ var WebSvr = (function(){ //Explose API //Filter - self.filter = Filter.add; + self.filter = Filter.filter; self.file = Filter.file; //Handler @@ -778,10 +796,21 @@ var webSvr = new WebSvr({https:true, httpsPort:8443}); webSvr.start(); /* -Filter: test/* => (session validation function); - parse:parse the post data and stored in req.body; +General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ +webSvr.filter(function(req, res){ + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(req, res); +}, {parse:true, session:true}); + +/* +Session Filter: protect test/* folder => (validation by session); +*/ webSvr.filter(/test\/[\w\.]+/, function(req, res){ //It's not index.htm/login.do, do the session validation if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ @@ -790,7 +819,7 @@ webSvr.filter(/test\/[\w\.]+/, function(req, res){ //Link to next filter req.filter.next(req, res); -}, {parse: true, session: true}); +}); /* diff --git a/svr/WebSvr.js b/svr/WebSvr.js index ba58fd3..9a6e396 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -134,11 +134,11 @@ var WebSvr = (function(){ //Rewrite the write/writeHead functionalities of current response object var endFn = res.end; res.end = function(){ - //Execute old end; + //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object res.write = res.writeHead = function(){ - console.log("response is already end, response write ignored!") + console.log("response is already end, response.write ignored!") }; }; @@ -169,7 +169,7 @@ var WebSvr = (function(){ //Explose API //Filter - self.filter = Filter.add; + self.filter = Filter.filter; self.file = Filter.file; //Handler From 913f37018a61becb0d271920394640ae8740d32d Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 1 Nov 2012 21:50:30 +0800 Subject: [PATCH 019/195] listDir: Add list directory functionality i.e. var webSvr = new WebSvr({ listDir: true }); --- svr/Settings.js | 3 ++ svr/SiteTest.js | 8 ++- svr/WebSvr.all.js | 131 ++++++++++++++++++++++++++++------------------ svr/WebSvr.js | 120 ++++++++++++++++++++++++------------------ 4 files changed, 158 insertions(+), 104 deletions(-) diff --git a/svr/Settings.js b/svr/Settings.js index 2da4c87..f8b7318 100644 --- a/svr/Settings.js +++ b/svr/Settings.js @@ -7,6 +7,9 @@ var Settings = { //root folder of web root: "../", + //list files in directory + listDir: false, + //http http: true, //default port of http diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 71de9bf..f36852e 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -1,7 +1,13 @@ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({https:true, httpsPort:8443}); +var webSvr = new WebSvr({ + root: "../", + listDir: true, + https: true, + httpsPort: 8443 +}); + webSvr.start(); /* diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index fccf645..cee1cbd 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -17,6 +17,9 @@ var Settings = { //root folder of web root: "../", + //list files in directory + listDir: false, + //http http: true, //default port of http @@ -558,7 +561,6 @@ var WebSvr = (function(){ //Parameters //Count: How many files? var self = this, - count = 0, root, port; @@ -599,50 +601,9 @@ var WebSvr = (function(){ fs.stat(fullPath, function(err, stat){ - count = 0; - //Consider as file not found if(err) return self.write404(res); - //List all the files in a directory. - var listFiles = function(callback){ - - fs.readdir(fullPath, function(err, files){ - if(err){ - console.log(err); - return; - } - - for(var idx = 0, len = files.length; idx < len; idx++){ - //Persistent the idx before make the sync process - (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - count++; - - if(err){ - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - count == len && callback(); - }); - })(idx); - } - - //If it's an empty directory - (len == 0) && callback(); - }); - }; - //Is file? Open this file and send to client. if(stat.isFile()){ writeFile(res, fullPath); @@ -650,14 +611,11 @@ var WebSvr = (function(){ //Is Directory? List all the files and folders. else if(stat.isDirectory()){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -          listFiles(function(){
    -            res.write("

    "); - res.end("
    Count: " + count + "
    "); - }); + if(options.listDir){ + self.listDir(req, res, fullPath); + }else{ + self.write403(res); + } } }); @@ -716,7 +674,65 @@ var WebSvr = (function(){ return path.join(root, filePath); }; - //Write file, filePath is a relative; + //List all the files in a directory + self.listDir = function(req, res, dir){ + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function(){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function(){
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files){ + if(err){ + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++){ + //Persistent the idx before make the sync process + (function(idx){ + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + cur++; + + if(err){ + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + }; + + //Write file, filePath is relative path self.writeFile = function(res, filePath, cb){ filePath = path.join(root, filePath); fs.exists(filePath, function(exist){ @@ -730,6 +746,11 @@ var WebSvr = (function(){ }); }; + self.write403 = function(res){ + res.writeHead(403, {"Content-Type": "text/html"}); + res.end("Access forbidden!"); + }; + self.write404 = function(res){ res.writeHead(404, {"Content-Type": "text/html"}); res.end("File not found!"); @@ -792,7 +813,13 @@ var WebSvr = (function(){ //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({https:true, httpsPort:8443}); +var webSvr = new WebSvr({ + root: "../", + listDir: false, + https:true, + httpsPort:8443 +}); + webSvr.start(); /* diff --git a/svr/WebSvr.js b/svr/WebSvr.js index 9a6e396..8b790ff 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -24,7 +24,6 @@ var WebSvr = (function(){ //Parameters //Count: How many files? var self = this, - count = 0, root, port; @@ -65,50 +64,9 @@ var WebSvr = (function(){ fs.stat(fullPath, function(err, stat){ - count = 0; - //Consider as file not found if(err) return self.write404(res); - //List all the files in a directory. - var listFiles = function(callback){ - - fs.readdir(fullPath, function(err, files){ - if(err){ - console.log(err); - return; - } - - for(var idx = 0, len = files.length; idx < len; idx++){ - //Persistent the idx before make the sync process - (function(idx){ - var filePath = path.join(fullPath, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - count++; - - if(err){ - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - count == len && callback(); - }); - })(idx); - } - - //If it's an empty directory - (len == 0) && callback(); - }); - }; - //Is file? Open this file and send to client. if(stat.isFile()){ writeFile(res, fullPath); @@ -116,14 +74,11 @@ var WebSvr = (function(){ //Is Directory? List all the files and folders. else if(stat.isDirectory()){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -          listFiles(function(){
    -            res.write("

    "); - res.end("
    Count: " + count + "
    "); - }); + if(options.listDir){ + self.listDir(req, res, fullPath); + }else{ + self.write403(res); + } } }); @@ -182,7 +137,65 @@ var WebSvr = (function(){ return path.join(root, filePath); }; - //Write file, filePath is a relative; + //List all the files in a directory + self.listDir = function(req, res, dir){ + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function(){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function(){
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files){ + if(err){ + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++){ + //Persistent the idx before make the sync process + (function(idx){ + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + cur++; + + if(err){ + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + }; + + //Write file, filePath is relative path self.writeFile = function(res, filePath, cb){ filePath = path.join(root, filePath); fs.exists(filePath, function(exist){ @@ -196,6 +209,11 @@ var WebSvr = (function(){ }); }; + self.write403 = function(res){ + res.writeHead(403, {"Content-Type": "text/html"}); + res.end("Access forbidden!"); + }; + self.write404 = function(res){ res.writeHead(404, {"Content-Type": "text/html"}); res.end("File not found!"); From fbee9eaeba7d0c8f189113624a16dee08989bf28 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 2 Nov 2012 14:13:32 +0800 Subject: [PATCH 020/195] ListDir: Separate the ListDir.js from WebSvr.js Makefile.list: Handler.js +ListDir.js WebSvr.js --- svr/Global.js | 7 +- svr/ListDir.js | 97 +++++++++++ svr/MakeFile.list | 1 + svr/WebSvr.all.js | 418 +++++++++++++++++++++++----------------------- svr/WebSvr.js | 111 +----------- 5 files changed, 323 insertions(+), 311 deletions(-) create mode 100644 svr/ListDir.js diff --git a/svr/Global.js b/svr/Global.js index a0554fe..5af97a2 100644 --- a/svr/Global.js +++ b/svr/Global.js @@ -5,4 +5,9 @@ */ //Underscore global object; -var _ = require("./lib/underscore"); \ No newline at end of file +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); +*/ \ No newline at end of file diff --git a/svr/ListDir.js b/svr/ListDir.js new file mode 100644 index 0000000..fc520ee --- /dev/null +++ b/svr/ListDir.js @@ -0,0 +1,97 @@ +/* +ListDir: List all the files in a directory +*/ +var ListDir = (function(){ + + var fs = require("fs"), + path = require("path"); + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url){ + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list : function(req, res, dir){ + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function(){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function(){
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files){ + if(err){ + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++){ + //Persistent the idx before make the sync process + (function(idx){ + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + cur++; + + if(err){ + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + +}()); \ No newline at end of file diff --git a/svr/MakeFile.list b/svr/MakeFile.list index bba8bb0..0845803 100644 --- a/svr/MakeFile.list +++ b/svr/MakeFile.list @@ -16,6 +16,7 @@ SessionParser.js Parser.js Filter.js Handler.js +ListDir.js WebSvr.js #Testing Site diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index cee1cbd..8d49484 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -1,12 +1,17 @@ /*Global.js*/ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); */ - -//Underscore global object; -var _ = require("./lib/underscore"); /*Settings.js*/ /* Configurations @@ -41,98 +46,98 @@ var Settings = { }; /*ref\Math.uuid.js*/ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); /*Mapper.js*/ /* @@ -534,6 +539,104 @@ var Handler; +/*ListDir.js*/ +/* +ListDir: List all the files in a directory +*/ +var ListDir = (function(){ + + var fs = require("fs"), + path = require("path"); + + var urlFormat = function(url){ + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date){ + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num){ + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url){ + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list : function(req, res, dir){ + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function(){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function(){
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files){ + if(err){ + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++){ + //Persistent the idx before make the sync process + (function(idx){ + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat){ + cur++; + + if(err){ + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + +}()); /*WebSvr.js*/ /* * Description: Create a Web Server (http based). @@ -544,19 +647,11 @@ var Handler; */ var WebSvr = (function(){ - var defaults = { - //Server port - port: 8054, - //Root path - root: "./../web", - session: false, - }; - var server = function(options){ //Library var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); + path = require("path"), + mime = require("./lib/mime"); //Parameters //Count: How many files? @@ -564,31 +659,6 @@ var WebSvr = (function(){ root, port; - var urlFormat = function(url){ - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date){ - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num){ - return num + " ".substring(0, 12 - String(num).length); - }; - - var anchor = function(txt, url){ - url = url ? url : "/"; - return '' + txt + ""; - }; - var fileHandler = function(req, res){ var url = req.url, @@ -611,11 +681,9 @@ var WebSvr = (function(){ //Is Directory? List all the files and folders. else if(stat.isDirectory()){ - if(options.listDir){ - self.listDir(req, res, fullPath); - }else{ - self.write403(res); - } + options.listDir + ? ListDir.list(req, res, fullPath) + : self.write403(res); } }); @@ -674,64 +742,6 @@ var WebSvr = (function(){ return path.join(root, filePath); }; - //List all the files in a directory - self.listDir = function(req, res, dir){ - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function(){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function(){
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files){ - if(err){ - listEnd(); - console.log(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++){ - //Persistent the idx before make the sync process - (function(idx){ - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - cur++; - - if(err){ - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - }; - //Write file, filePath is relative path self.writeFile = function(res, filePath, cb){ filePath = path.join(root, filePath); @@ -792,16 +802,12 @@ var WebSvr = (function(){ self.httpsSvr = httpsSvr; } - }; //Public: close http server; self.close = function(){ - if(self.httpSvr){ - self.httpSvr.close(); - return true; - } - return false; + self.httpSvr && self.httpSvr.close(); + self.httpsSvr && self.httpsSvr.close(); }; }; @@ -815,9 +821,9 @@ var WebSvr = (function(){ //Trying at: http://localhost:8054 var webSvr = new WebSvr({ root: "../", - listDir: false, - https:true, - httpsPort:8443 + listDir: true, + https: true, + httpsPort: 8443 }); webSvr.start(); diff --git a/svr/WebSvr.js b/svr/WebSvr.js index 8b790ff..36237a2 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -7,19 +7,11 @@ */ var WebSvr = (function(){ - var defaults = { - //Server port - port: 8054, - //Root path - root: "./../web", - session: false, - }; - var server = function(options){ //Library var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); + path = require("path"), + mime = require("./lib/mime"); //Parameters //Count: How many files? @@ -27,31 +19,6 @@ var WebSvr = (function(){ root, port; - var urlFormat = function(url){ - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date){ - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num){ - return num + " ".substring(0, 12 - String(num).length); - }; - - var anchor = function(txt, url){ - url = url ? url : "/"; - return '' + txt + ""; - }; - var fileHandler = function(req, res){ var url = req.url, @@ -74,11 +41,9 @@ var WebSvr = (function(){ //Is Directory? List all the files and folders. else if(stat.isDirectory()){ - if(options.listDir){ - self.listDir(req, res, fullPath); - }else{ - self.write403(res); - } + options.listDir + ? ListDir.list(req, res, fullPath) + : self.write403(res); } }); @@ -137,64 +102,6 @@ var WebSvr = (function(){ return path.join(root, filePath); }; - //List all the files in a directory - self.listDir = function(req, res, dir){ - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function(){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function(){
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files){ - if(err){ - listEnd(); - console.log(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++){ - //Persistent the idx before make the sync process - (function(idx){ - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat){ - cur++; - - if(err){ - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - }; - //Write file, filePath is relative path self.writeFile = function(res, filePath, cb){ filePath = path.join(root, filePath); @@ -255,16 +162,12 @@ var WebSvr = (function(){ self.httpsSvr = httpsSvr; } - }; //Public: close http server; self.close = function(){ - if(self.httpSvr){ - self.httpSvr.close(); - return true; - } - return false; + self.httpSvr && self.httpSvr.close(); + self.httpsSvr && self.httpsSvr.close(); }; }; From cfb7e6356fcd467b1a75d13b944aa974740e0e15 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 2 Nov 2012 17:09:04 +0800 Subject: [PATCH 021/195] Redirect: transfer request 301/302 Redirect request: webSvr.url(url, function(req, res){ res.redirect(newUrl [, statusCode] ); }); --- svr/SiteTest.js | 17 +++- svr/WebSvr.all.js | 234 +++++++++++++++++++++++++--------------------- svr/WebSvr.js | 9 ++ 3 files changed, 146 insertions(+), 114 deletions(-) diff --git a/svr/SiteTest.js b/svr/SiteTest.js index f36852e..8b13652 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -42,7 +42,7 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.session(/login.do/, function(req, res){ +webSvr.session("login.do", function(req, res){ var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); @@ -64,7 +64,7 @@ webSvr.session(/login.do/, function(req, res){ /* Uploader: upload.do => (receive handler) */ -webSvr.file(/upload.do/, function(req, res){ +webSvr.file("upload.do", function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -72,16 +72,23 @@ webSvr.file(/upload.do/, function(req, res){ res.end(JSON.stringify(req.files)); }); +/* +Redirect: redirect request, try at: http://localhost:8054/redirect +*/ +webSvr.url("redirect", function(req, res){ + res.redirect("/svr/websvr.all.js"); +}); + /* Simple redirect API: */ //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine -webSvr.url(/combine/, ["svr/tool/Combine.js"]); +webSvr.url("combine", ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello -webSvr.url(/hello/, "Hello WebSvr!"); +webSvr.url("hello", "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post(/post.htm/, function(req, res){ +webSvr.post("post.htm", function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); //Witch session support: "{session: true}" res.write("You username is " + req.session.get("username")); diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index 8d49484..19a66d1 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -1,16 +1,16 @@ /*Global.js*/ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang -*/ - -//Underscore global object; -var _ = require("./lib/underscore"); - -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); */ /*Settings.js*/ /* @@ -46,98 +46,98 @@ var Settings = { }; /*ref\Math.uuid.js*/ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); /*Mapper.js*/ /* @@ -706,6 +706,11 @@ var WebSvr = (function(){ self.writeFile(res, filePath, cb); }; + res.redirect = function(url, status){ + res.writeHead(status ? status : 302, { "Location": url }); + res.end(); + }; + //Define filter object req.filter = new FilterChain(function(){ //if handler not match, send the request @@ -756,6 +761,10 @@ var WebSvr = (function(){ }); }; + //TODO: Support 301 move permanently + + //TODO: Support 304 client-side cache + self.write403 = function(res){ res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); @@ -860,7 +869,7 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.session(/login.do/, function(req, res){ +webSvr.session("login.do", function(req, res){ var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); @@ -882,7 +891,7 @@ webSvr.session(/login.do/, function(req, res){ /* Uploader: upload.do => (receive handler) */ -webSvr.file(/upload.do/, function(req, res){ +webSvr.file("upload.do", function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -890,16 +899,23 @@ webSvr.file(/upload.do/, function(req, res){ res.end(JSON.stringify(req.files)); }); +/* +Redirect: redirect request, try at: http://localhost:8054/redirect +*/ +webSvr.url("redirect", function(req, res){ + res.redirect("/svr/websvr.all.js"); +}); + /* Simple redirect API: */ //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine -webSvr.url(/combine/, ["svr/tool/Combine.js"]); +webSvr.url("combine", ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello -webSvr.url(/hello/, "Hello WebSvr!"); +webSvr.url("hello", "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post(/post.htm/, function(req, res){ +webSvr.post("post.htm", function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); //Witch session support: "{session: true}" res.write("You username is " + req.session.get("username")); diff --git a/svr/WebSvr.js b/svr/WebSvr.js index 36237a2..40fe126 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -66,6 +66,11 @@ var WebSvr = (function(){ self.writeFile(res, filePath, cb); }; + res.redirect = function(url, status){ + res.writeHead(status ? status : 302, { "Location": url }); + res.end(); + }; + //Define filter object req.filter = new FilterChain(function(){ //if handler not match, send the request @@ -116,6 +121,10 @@ var WebSvr = (function(){ }); }; + //TODO: Support 301 move permanently + + //TODO: Support 304 client-side cache + self.write403 = function(res){ res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); From 2bb2ac885008c12e09fdd4689a2544f418710023 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 3 Nov 2012 09:06:26 +0800 Subject: [PATCH 022/195] Update README Add more description --- README.md | 81 ++++++++++++++++++++++++++++++++++++--------------- svr/Global.js | 29 ++++++++++-------- 2 files changed, 74 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f8389b9..0bb8257 100644 --- a/README.md +++ b/README.md @@ -4,35 +4,50 @@ Lincense: MIT, GPL Description: -------------- -Create a Web Server (http based), all the files will be combined into one, in order to use the various kinds of javascript libraries in Node.js; - +A simple web server based on node.js Version: 0.024 -------------- -- Filter: A request will mapping all the filters first, and then pass to the Handler; -- Handler: When a request matched a handler, it will returned, so only one handler will be matched; -- Session: Stored in file, with JSON format; -- Form Data: Support upload files, integrate - https://github.com/felixge/node-formidable/ -- Underscore: Add underscore a utility-belt library for JavaScript - https://github.com/documentcloud/underscore -- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime - -Start: Edit in SiteTest.js or Create a new Site.js and added to MakeFile.list +- Filter: A request will try to match all the filters first, and then pass to the Handler +- Handler: When a request matched a handler, it will returned, only one handler will be executed +- Session: Stored in file, with JSON format +- File: Support uploading files + +Start -------------- +Edit in SiteTest.js or Create a new Site.js and added to MakeFile.list - //Start WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; + //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 - var webSvr = new WebSvr({root:"./../"}); + var webSvr = new WebSvr({ + root: "../", + listDir: true, + https: true, + httpsPort: 8443 + }); + webSvr.start(); -Filter: Session based authentication (session stored in files), all the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" +Filter -------------- +Session based authentication (session stored in files), all the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" + /* - Filter: test/* => (session validation function); - parse:parse the post data and stored to req.body; + General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ + webSvr.filter(function(req, res){ + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(req, res); + }, {parse:true, session:true}); + + /* + Session Filter: protect test/* folder => (validation by session); + */ webSvr.filter(/test\/[\w\.]+/, function(req, res){ //It's not index.htm/login.do, do the session validation if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ @@ -41,16 +56,19 @@ Filter: Session based authentication (session stored in files), all the request //Link to next filter req.filter.next(req, res); - }, {parse: true, session: true}); + }); -Handler: Handle Login and update the username in Session + +Handler -------------- +Handle Login and put the username in Session + /* Handler: login.do => (validate the username & password) username: admin password: 12345678 */ - webSvr.session(/login.do/, function(req, res){ + webSvr.session("login.do", function(req, res){ var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); @@ -69,13 +87,14 @@ Handler: Handle Login and update the username in Session } }); - -File: Receive upload file (it's a specfic filter) +File -------------- +Receive upload file (it's a specfic filter) + /* Uploader: upload.do => (receive handler) */ - webSvr.file(/upload.do/, function(req, res){ + webSvr.file("upload.do", function(req, res){ res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -84,12 +103,26 @@ File: Receive upload file (it's a specfic filter) }); -Handler: Other simple redirect API +Simple API -------------- +Redirect + + /* + Redirect: redirect request, try at: http://localhost:8054/redirect + */ + webSvr.url("redirect", function(req, res){ + res.redirect("/svr/websvr.all.js"); + }); + +Url Mapping + //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine webSvr.url(/combine/, ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello webSvr.url(/hello/, "Hello WebSvr!"); + +Post Data + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm webSvr.post(/post.htm/, function(req, res){ res.writeHead(200, {"Content-Type": "text/html"}); diff --git a/svr/Global.js b/svr/Global.js index 5af97a2..77499b3 100644 --- a/svr/Global.js +++ b/svr/Global.js @@ -1,13 +1,18 @@ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang -*/ - -//Underscore global object; -var _ = require("./lib/underscore"); - -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +- Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +- Underscore: Add underscore a utility-belt library for JavaScript + https://github.com/documentcloud/underscore +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); */ \ No newline at end of file From 19c8642de59ef62e6696c32615c423e3824b32b8 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 3 Nov 2012 09:11:28 +0800 Subject: [PATCH 023/195] Update README Correct type error --- README.md | 2 +- svr/WebSvr.all.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0bb8257..99ce6b6 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Session based authentication (session stored in files), all the request under "t }, {parse:true, session:true}); /* - Session Filter: protect test/* folder => (validation by session); + Session Filter: protect test/* folder => (validate by session); */ webSvr.filter(/test\/[\w\.]+/, function(req, res){ //It's not index.htm/login.do, do the session validation diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index 19a66d1..8134cec 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -3,6 +3,11 @@ * Description: WebSvr * Lincense: MIT, GPL * Author: Kris Zhang +- Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +- Underscore: Add underscore a utility-belt library for JavaScript + https://github.com/documentcloud/underscore +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime */ //Underscore global object; From 1ed2c579e7853729ded73f5f7ea42b974ad15668 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 6 Nov 2012 14:17:17 +0800 Subject: [PATCH 024/195] optimize formatting --- README.md | 22 +++-- svr/Filter.js | 18 ++-- svr/Handler.js | 16 ++-- svr/ListDir.js | 28 +++--- svr/Mapper.js | 12 +-- svr/Parser.js | 20 ++--- svr/RequestParser.js | 6 +- svr/SessionParser.js | 18 ++-- svr/Settings.js | 3 + svr/SiteTest.js | 21 ++--- svr/WebSvr.all.js | 199 ++++++++++++++++++++++--------------------- svr/WebSvr.js | 57 +++++++------ svr/tool/Combine.js | 88 +++++++++---------- 13 files changed, 267 insertions(+), 241 deletions(-) diff --git a/README.md b/README.md index 99ce6b6..fdbdc1a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Session based authentication (session stored in files), all the request under "t parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ +<<<<<<< HEAD webSvr.filter(function(req, res){ //TODO: Add greeting words in filter //res.write("Hello WebSvr!
    "); @@ -49,8 +50,11 @@ Session based authentication (session stored in files), all the request under "t Session Filter: protect test/* folder => (validate by session); */ webSvr.filter(/test\/[\w\.]+/, function(req, res){ +======= + webSvr.filter(/test\/[\w\.]+/, function(req, res) { +>>>>>>> optimize //It's not index.htm/login.do, do the session validation - if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { !req.session.get("username") && res.end("You must login, first!"); } @@ -68,15 +72,19 @@ Handle Login and put the username in Session username: admin password: 12345678 */ +<<<<<<< HEAD webSvr.session("login.do", function(req, res){ +======= + webSvr.session(/login.do/, function(req, res) { +>>>>>>> optimize var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); var qs = querystring.parse(req.body); - if(qs.username == "admin" && qs.password == "12345678"){ + if (qs.username == "admin" && qs.password == "12345678") { //Put key/value pair in session //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session){ + req.session.set("username", qs.username, function(session) { //TODO: Add req.redirect / req.forward functionalities; res.writeHead(200, {"Content-Type": "text/html"}); res.writeFile("/test/setting.htm"); @@ -94,7 +102,11 @@ Receive upload file (it's a specfic filter) /* Uploader: upload.do => (receive handler) */ +<<<<<<< HEAD webSvr.file("upload.do", function(req, res){ +======= + webSvr.file(/upload.do/, function(req, res) { +>>>>>>> optimize res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -103,7 +115,7 @@ Receive upload file (it's a specfic filter) }); -Simple API +Other APIs -------------- Redirect @@ -124,7 +136,7 @@ Url Mapping Post Data //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm - webSvr.post(/post.htm/, function(req, res){ + webSvr.post(/post.htm/, function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //Need session support res.write("You username is " + req.session.get("username")); diff --git a/svr/Filter.js b/svr/Filter.js index 2057b97..3497163 100644 --- a/svr/Filter.js +++ b/svr/Filter.js @@ -4,7 +4,7 @@ Filters will be always called before a handler. */ var Filter = { //filter list - filters : [], + filters: [], /* filter: add a new filter @@ -12,9 +12,9 @@ var Filter = { handler: function [required] options: object [optional] */ - filter : function(expression, handler, options){ + filter: function(expression, handler, options) { //The first parameter is Function => (handler, options) - if(expression.constructor == Function){ + if (expression.constructor == Function) { options = handler; handler = expression; expression = null; @@ -28,7 +28,7 @@ var Filter = { file receiver: it's a specfic filter, this filter should be always at the top of the filter list */ - file: function(expression, handler, options){ + file: function(expression, handler, options) { var mapper = new Mapper(expression, handler, {file: true}); //insert as the first elements Filter.filters.splice(0, 0, mapper); @@ -38,31 +38,31 @@ var Filter = { /* Filter Chain */ -var FilterChain = function(cb){ +var FilterChain = function(cb) { var self = this; self.idx = 0; self.cb = cb; }; FilterChain.prototype = { - next : function(req, res){ + next: function(req, res) { var self = this; var mapper = Filter.filters[self.idx++]; //filter is complete, execute callback; - if(!mapper) return self.cb && self.cb(); + if (!mapper) return self.cb && self.cb(); /* If not Matched go to next filter If matched need to execute the req.next() in callback handler, e.g: - webSvr.filter(/expression/, function(req, res){ + webSvr.filter(/expression/, function(req, res) { //filter actions req.next(req, res); }, options); */ - if(mapper.match(req)){ + if (mapper.match(req)) { console.log("filter matched", self.idx, mapper.expression, req.url); diff --git a/svr/Handler.js b/svr/Handler.js index 6be15c9..eda6d19 100644 --- a/svr/Handler.js +++ b/svr/Handler.js @@ -4,7 +4,7 @@ At the same time only one Handler will be called; */ var Handler; -(function(){ +(function() { /* Private: handler list @@ -21,34 +21,34 @@ var Handler; handler: [many types] [required] options: object [optional] */ - url : function(expression, handler, options){ + url: function(expression, handler, options) { var mapper = new Mapper(expression, handler, options); handlers.push(mapper); }, //Post: Parse the post data by default; - post : function(expression, handler, options){ + post: function(expression, handler, options) { this.url(expression, handler, _.extend({ parse: true }, options)); }, //Session: Parse the session and post by default; - session : function(expression, handler){ + session: function(expression, handler) { this.url(expression, handler, { parse: true, session: true }); }, - handle : function(req, res){ + handle: function(req, res) { //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++){ + for(var i = 0, len = handlers.length; i < len ; i++) { var mapper = handlers[i]; - if(mapper.match(req)){ + if (mapper.match(req)) { console.log("handler matched", i, mapper.expression, req.url); var handler = mapper.handler, type = handler.constructor.name; - switch(type){ + switch(type) { //function: treated it as custom function handler case "Function": Parser(req, res, mapper); diff --git a/svr/ListDir.js b/svr/ListDir.js index fc520ee..5d773f0 100644 --- a/svr/ListDir.js +++ b/svr/ListDir.js @@ -1,19 +1,19 @@ /* ListDir: List all the files in a directory */ -var ListDir = (function(){ +var ListDir = (function() { var fs = require("fs"), path = require("path"); - var urlFormat = function(url){ + var urlFormat = function(url) { url = url.replace(/\\/g,'/'); url = url.replace(/ /g,'%20'); return url; }; //Align to right - var date = function(date){ + var date = function(date) { var d = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + (date.getDay() + 1) @@ -22,39 +22,39 @@ var ListDir = (function(){ }; //Align to left - var size = function(num){ + var size = function(num) { return num + " ".substring(0, 12 - String(num).length); }; //Create an anchor - var anchor = function(txt, url){ + var anchor = function(txt, url) { url = url ? url : "/"; return '' + txt + ""; }; var listDir = { //List all the files in a directory - list : function(req, res, dir){ + list: function(req, res, dir) { var url = req.url, cur = 0, len = 0; - var listBegin = function(){ + var listBegin = function() { res.writeHead(200, {"Content-Type": "text/html"}); res.write("

    http://" + req.headers.host + url + "


    "); res.write("
    ");
             res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
           };
     
    -      var listEnd = function(){
    +      var listEnd = function() {
             res.write("

    "); res.end("
    Count: " + len + "
    "); }; listBegin(); - fs.readdir(dir, function(err, files){ - if(err){ + fs.readdir(dir, function(err, files) { + if (err) { listEnd(); console.log(err); return; @@ -62,16 +62,16 @@ var ListDir = (function(){ len = files.length; - for(var idx = 0; idx < len; idx++){ + for(var idx = 0; idx < len; idx++) { //Persistent the idx before make the sync process - (function(idx){ + (function(idx) { var filePath = path.join(dir, files[idx]), fileUrl = urlFormat(path.join(url, files[idx])); - fs.stat(filePath, function(err, stat){ + fs.stat(filePath, function(err, stat) { cur++; - if(err){ + if (err) { console.log(err); }else{ res.write( diff --git a/svr/Mapper.js b/svr/Mapper.js index f194dc0..2f31181 100644 --- a/svr/Mapper.js +++ b/svr/Mapper.js @@ -5,7 +5,7 @@ handler: required parameter options: other optional parameters */ -var Mapper = function(expression, handler, options){ +var Mapper = function(expression, handler, options) { var self = this; self.expression = expression; @@ -19,14 +19,14 @@ Mapper.prototype = { /* Does this mapper matched this request? */ - match: function(req){ + match: function(req) { var self = this, expression = self.expression; //No expression? It's a general filter mapper - if(!expression) return true; + if (!expression) return true; - switch(expression.constructor){ + switch(expression.constructor) { case String: return req.url.indexOf(expression) > -1; case RegExp: return expression.test(req.url); } @@ -41,8 +41,8 @@ Mapper.prototype = { file: boolean parse: boolean */ - extend: function(options){ - for(key in options){ + extend: function(options) { + for(key in options) { this[key] = options[key] } } diff --git a/svr/Parser.js b/svr/Parser.js index 2a6d7a6..33e8753 100644 --- a/svr/Parser.js +++ b/svr/Parser.js @@ -1,15 +1,15 @@ /* Parser: Functions that Filter and Handler will be called */ -var Parser = function(req, res, mapper){ +var Parser = function(req, res, mapper) { var handler = mapper.handler; //add sesion support - var parseSession = function(){ + var parseSession = function() { //add sesion support - if(mapper.session && typeof req.session == "undefined"){ - SessionParser(req, res, function(session){ + if (mapper.session && typeof req.session == "undefined") { + SessionParser(req, res, function(session) { req.session = session; handler(req, res); }); @@ -22,11 +22,11 @@ var Parser = function(req, res, mapper){ parse data in request, this should be done before parse session, because session stored in file */ - var parseRequest = function(){ + var parseRequest = function() { //need to parse the request? - if(mapper.parse && typeof req.body == "undefined"){ + if (mapper.parse && typeof req.body == "undefined") { //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data){ + RequestParser(req, res, function(data) { req.body = data; parseSession(); }); @@ -38,9 +38,9 @@ var Parser = function(req, res, mapper){ /* parse file in request, this should be at the top of the list */ - var parseFile = function(){ + var parseFile = function() { //Need to parse the file in request? - if(mapper.file && typeof req.body == "undefined"){ + if (mapper.file && typeof req.body == "undefined") { //Must parser the request first, or the post data maybe lost; var formidable = require('./lib/incoming_form'), form = new formidable.IncomingForm(); @@ -48,7 +48,7 @@ var Parser = function(req, res, mapper){ form.uploadDir = Settings.uploadDir; form.parse(req, function(err, fields, files) { - if (err){ + if (err) { console.log(err); return; }; diff --git a/svr/RequestParser.js b/svr/RequestParser.js index 5784e8c..2794277 100644 --- a/svr/RequestParser.js +++ b/svr/RequestParser.js @@ -4,13 +4,13 @@ when parse complete, execute the callback, with response data; */ var RequestParser; -(function(){ +(function() { - //TODO: Is there a bug, how about 2 users update a file, what's will happened for buffer; + //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? var MAX_SIZE = 16 * 1024 * 1024, buffer = new Buffer(MAX_SIZE); - RequestParser = function(req, res, callback){ + RequestParser = function(req, res, callback) { var length = 0, data = ""; req.on('data', function(chunk) { diff --git a/svr/SessionParser.js b/svr/SessionParser.js index 7baf511..4ece4e6 100644 --- a/svr/SessionParser.js +++ b/svr/SessionParser.js @@ -2,11 +2,11 @@ var SessionParser; //TODO: Need a child process of clear session -(function(){ +(function() { var fs = require("fs"); - SessionParser = (function(req, res, callback){ + SessionParser = (function(req, res, callback) { var sessionDir = Settings.sessionDir; @@ -18,14 +18,14 @@ var SessionParser; }; //TODO - self.set = function(key, val, callback){ + self.set = function(key, val, callback) { var sessionfile = sessionDir + self.sid; key && (self.obj[key] = val); - fs.writeFile( sessionfile, JSON.stringify(self.obj), function(err){ - if(err){ + fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { + if (err) { console.log(err); return; } @@ -35,11 +35,11 @@ var SessionParser; }; //TO DO - self.get = function(key){ + self.get = function(key) { return self.obj[key]; }; - self.init = function(){ + self.init = function() { var sidKey = "_wsid", sidVal, cookie = req.headers.cookie || ""; @@ -51,7 +51,7 @@ var SessionParser; (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); //sid doesn't exist, create it; - if(idx < 0 || sidVal.length != 32){ + if (idx < 0 || sidVal.length != 32) { sidVal = Math.uuid(32); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); }; @@ -62,7 +62,7 @@ var SessionParser; //here will be cause a bit of delay fs.exists(sessionfile, function (exists) { - if(exists){ + if (exists) { fs.readFile( sessionfile, function (err, data) { if (err) { console.log(err); diff --git a/svr/Settings.js b/svr/Settings.js index f8b7318..ce80b68 100644 --- a/svr/Settings.js +++ b/svr/Settings.js @@ -15,6 +15,9 @@ var Settings = { //default port of http port: 8054, + //enable debug information output? + debug: false, + //https https: false, //default port of https diff --git a/svr/SiteTest.js b/svr/SiteTest.js index 8b13652..2d9abfd 100644 --- a/svr/SiteTest.js +++ b/svr/SiteTest.js @@ -5,7 +5,8 @@ var webSvr = new WebSvr({ root: "../", listDir: true, https: true, - httpsPort: 8443 + httpsPort: 8443, + debug: true }); webSvr.start(); @@ -15,7 +16,7 @@ General filter: parse the post data / session before all request parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ -webSvr.filter(function(req, res){ +webSvr.filter(function(req, res) { //TODO: Add greeting words in filter //res.write("Hello WebSvr!
    "); @@ -26,9 +27,9 @@ webSvr.filter(function(req, res){ /* Session Filter: protect test/* folder => (validation by session); */ -webSvr.filter(/test\/[\w\.]+/, function(req, res){ +webSvr.filter(/test\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation - if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { !req.session.get("username") && res.end("You must login, first!"); } @@ -42,15 +43,15 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.session("login.do", function(req, res){ +webSvr.session("login.do", function(req, res) { var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); var qs = querystring.parse(req.body); - if(qs.username == "admin" && qs.password == "12345678"){ + if (qs.username == "admin" && qs.password == "12345678") { //Put key/value pair in session //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session){ + req.session.set("username", qs.username, function(session) { //TODO: Add req.redirect / req.forward functionalities; res.writeHead(200, {"Content-Type": "text/html"}); res.writeFile("/test/setting.htm"); @@ -64,7 +65,7 @@ webSvr.session("login.do", function(req, res){ /* Uploader: upload.do => (receive handler) */ -webSvr.file("upload.do", function(req, res){ +webSvr.file("upload.do", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -75,7 +76,7 @@ webSvr.file("upload.do", function(req, res){ /* Redirect: redirect request, try at: http://localhost:8054/redirect */ -webSvr.url("redirect", function(req, res){ +webSvr.url("redirect", function(req, res) { res.redirect("/svr/websvr.all.js"); }); @@ -88,7 +89,7 @@ webSvr.url("combine", ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello webSvr.url("hello", "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post("post.htm", function(req, res){ +webSvr.post("post.htm", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //Witch session support: "{session: true}" res.write("You username is " + req.session.get("username")); diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js index 8134cec..6d778f2 100644 --- a/svr/WebSvr.all.js +++ b/svr/WebSvr.all.js @@ -35,6 +35,9 @@ var Settings = { //default port of http port: 8054, + //enable debug information output? + debug: false, + //https https: false, //default port of https @@ -152,7 +155,7 @@ handler: required parameter options: other optional parameters */ -var Mapper = function(expression, handler, options){ +var Mapper = function(expression, handler, options) { var self = this; self.expression = expression; @@ -166,14 +169,14 @@ Mapper.prototype = { /* Does this mapper matched this request? */ - match: function(req){ + match: function(req) { var self = this, expression = self.expression; //No expression? It's a general filter mapper - if(!expression) return true; + if (!expression) return true; - switch(expression.constructor){ + switch(expression.constructor) { case String: return req.url.indexOf(expression) > -1; case RegExp: return expression.test(req.url); } @@ -188,8 +191,8 @@ Mapper.prototype = { file: boolean parse: boolean */ - extend: function(options){ - for(key in options){ + extend: function(options) { + for(key in options) { this[key] = options[key] } } @@ -201,13 +204,13 @@ when parse complete, execute the callback, with response data; */ var RequestParser; -(function(){ +(function() { - //TODO: Is there a bug, how about 2 users update a file, what's will happened for buffer; + //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? var MAX_SIZE = 16 * 1024 * 1024, buffer = new Buffer(MAX_SIZE); - RequestParser = function(req, res, callback){ + RequestParser = function(req, res, callback) { var length = 0, data = ""; req.on('data', function(chunk) { @@ -227,11 +230,11 @@ var RequestParser; var SessionParser; //TODO: Need a child process of clear session -(function(){ +(function() { var fs = require("fs"); - SessionParser = (function(req, res, callback){ + SessionParser = (function(req, res, callback) { var sessionDir = Settings.sessionDir; @@ -243,14 +246,14 @@ var SessionParser; }; //TODO - self.set = function(key, val, callback){ + self.set = function(key, val, callback) { var sessionfile = sessionDir + self.sid; key && (self.obj[key] = val); - fs.writeFile( sessionfile, JSON.stringify(self.obj), function(err){ - if(err){ + fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { + if (err) { console.log(err); return; } @@ -260,11 +263,11 @@ var SessionParser; }; //TO DO - self.get = function(key){ + self.get = function(key) { return self.obj[key]; }; - self.init = function(){ + self.init = function() { var sidKey = "_wsid", sidVal, cookie = req.headers.cookie || ""; @@ -276,7 +279,7 @@ var SessionParser; (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); //sid doesn't exist, create it; - if(idx < 0 || sidVal.length != 32){ + if (idx < 0 || sidVal.length != 32) { sidVal = Math.uuid(32); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); }; @@ -287,7 +290,7 @@ var SessionParser; //here will be cause a bit of delay fs.exists(sessionfile, function (exists) { - if(exists){ + if (exists) { fs.readFile( sessionfile, function (err, data) { if (err) { console.log(err); @@ -318,15 +321,15 @@ var SessionParser; /* Parser: Functions that Filter and Handler will be called */ -var Parser = function(req, res, mapper){ +var Parser = function(req, res, mapper) { var handler = mapper.handler; //add sesion support - var parseSession = function(){ + var parseSession = function() { //add sesion support - if(mapper.session && typeof req.session == "undefined"){ - SessionParser(req, res, function(session){ + if (mapper.session && typeof req.session == "undefined") { + SessionParser(req, res, function(session) { req.session = session; handler(req, res); }); @@ -339,11 +342,11 @@ var Parser = function(req, res, mapper){ parse data in request, this should be done before parse session, because session stored in file */ - var parseRequest = function(){ + var parseRequest = function() { //need to parse the request? - if(mapper.parse && typeof req.body == "undefined"){ + if (mapper.parse && typeof req.body == "undefined") { //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data){ + RequestParser(req, res, function(data) { req.body = data; parseSession(); }); @@ -355,9 +358,9 @@ var Parser = function(req, res, mapper){ /* parse file in request, this should be at the top of the list */ - var parseFile = function(){ + var parseFile = function() { //Need to parse the file in request? - if(mapper.file && typeof req.body == "undefined"){ + if (mapper.file && typeof req.body == "undefined") { //Must parser the request first, or the post data maybe lost; var formidable = require('./lib/incoming_form'), form = new formidable.IncomingForm(); @@ -365,7 +368,7 @@ var Parser = function(req, res, mapper){ form.uploadDir = Settings.uploadDir; form.parse(req, function(err, fields, files) { - if (err){ + if (err) { console.log(err); return; }; @@ -393,7 +396,7 @@ Filters will be always called before a handler. */ var Filter = { //filter list - filters : [], + filters: [], /* filter: add a new filter @@ -401,9 +404,9 @@ var Filter = { handler: function [required] options: object [optional] */ - filter : function(expression, handler, options){ + filter: function(expression, handler, options) { //The first parameter is Function => (handler, options) - if(expression.constructor == Function){ + if (expression.constructor == Function) { options = handler; handler = expression; expression = null; @@ -417,7 +420,7 @@ var Filter = { file receiver: it's a specfic filter, this filter should be always at the top of the filter list */ - file: function(expression, handler, options){ + file: function(expression, handler, options) { var mapper = new Mapper(expression, handler, {file: true}); //insert as the first elements Filter.filters.splice(0, 0, mapper); @@ -427,31 +430,31 @@ var Filter = { /* Filter Chain */ -var FilterChain = function(cb){ +var FilterChain = function(cb) { var self = this; self.idx = 0; self.cb = cb; }; FilterChain.prototype = { - next : function(req, res){ + next: function(req, res) { var self = this; var mapper = Filter.filters[self.idx++]; //filter is complete, execute callback; - if(!mapper) return self.cb && self.cb(); + if (!mapper) return self.cb && self.cb(); /* If not Matched go to next filter If matched need to execute the req.next() in callback handler, e.g: - webSvr.filter(/expression/, function(req, res){ + webSvr.filter(/expression/, function(req, res) { //filter actions req.next(req, res); }, options); */ - if(mapper.match(req)){ + if (mapper.match(req)) { console.log("filter matched", self.idx, mapper.expression, req.url); @@ -468,7 +471,7 @@ At the same time only one Handler will be called; */ var Handler; -(function(){ +(function() { /* Private: handler list @@ -485,34 +488,34 @@ var Handler; handler: [many types] [required] options: object [optional] */ - url : function(expression, handler, options){ + url: function(expression, handler, options) { var mapper = new Mapper(expression, handler, options); handlers.push(mapper); }, //Post: Parse the post data by default; - post : function(expression, handler, options){ + post: function(expression, handler, options) { this.url(expression, handler, _.extend({ parse: true }, options)); }, //Session: Parse the session and post by default; - session : function(expression, handler){ + session: function(expression, handler) { this.url(expression, handler, { parse: true, session: true }); }, - handle : function(req, res){ + handle: function(req, res) { //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++){ + for(var i = 0, len = handlers.length; i < len ; i++) { var mapper = handlers[i]; - if(mapper.match(req)){ + if (mapper.match(req)) { console.log("handler matched", i, mapper.expression, req.url); var handler = mapper.handler, type = handler.constructor.name; - switch(type){ + switch(type) { //function: treated it as custom function handler case "Function": Parser(req, res, mapper); @@ -548,19 +551,19 @@ var Handler; /* ListDir: List all the files in a directory */ -var ListDir = (function(){ +var ListDir = (function() { var fs = require("fs"), path = require("path"); - var urlFormat = function(url){ + var urlFormat = function(url) { url = url.replace(/\\/g,'/'); url = url.replace(/ /g,'%20'); return url; }; //Align to right - var date = function(date){ + var date = function(date) { var d = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + (date.getDay() + 1) @@ -569,39 +572,39 @@ var ListDir = (function(){ }; //Align to left - var size = function(num){ + var size = function(num) { return num + " ".substring(0, 12 - String(num).length); }; //Create an anchor - var anchor = function(txt, url){ + var anchor = function(txt, url) { url = url ? url : "/"; return '' + txt + ""; }; var listDir = { //List all the files in a directory - list : function(req, res, dir){ + list: function(req, res, dir) { var url = req.url, cur = 0, len = 0; - var listBegin = function(){ + var listBegin = function() { res.writeHead(200, {"Content-Type": "text/html"}); res.write("

    http://" + req.headers.host + url + "


    "); res.write("
    ");
             res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
           };
     
    -      var listEnd = function(){
    +      var listEnd = function() {
             res.write("

    "); res.end("
    Count: " + len + "
    "); }; listBegin(); - fs.readdir(dir, function(err, files){ - if(err){ + fs.readdir(dir, function(err, files) { + if (err) { listEnd(); console.log(err); return; @@ -609,16 +612,16 @@ var ListDir = (function(){ len = files.length; - for(var idx = 0; idx < len; idx++){ + for(var idx = 0; idx < len; idx++) { //Persistent the idx before make the sync process - (function(idx){ + (function(idx) { var filePath = path.join(dir, files[idx]), fileUrl = urlFormat(path.join(url, files[idx])); - fs.stat(filePath, function(err, stat){ + fs.stat(filePath, function(err, stat) { cur++; - if(err){ + if (err) { console.log(err); }else{ res.write( @@ -650,9 +653,9 @@ var ListDir = (function(){ /* * WebSvr Namespace */ -var WebSvr = (function(){ +var WebSvr = (function() { - var server = function(options){ + var server = function(options) { //Library var fs = require("fs"), path = require("path"), @@ -664,7 +667,7 @@ var WebSvr = (function(){ root, port; - var fileHandler = function(req, res){ + var fileHandler = function(req, res) { var url = req.url, hasQuery = url.indexOf("?"); @@ -674,18 +677,18 @@ var WebSvr = (function(){ var fullPath = path.join(root, url); - fs.stat(fullPath, function(err, stat){ + fs.stat(fullPath, function(err, stat) { //Consider as file not found - if(err) return self.write404(res); + if (err) return self.write404(res); //Is file? Open this file and send to client. - if(stat.isFile()){ + if (stat.isFile()) { writeFile(res, fullPath); } //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ + else if (stat.isDirectory()) { options.listDir ? ListDir.list(req, res, fullPath) : self.write403(res); @@ -694,30 +697,30 @@ var WebSvr = (function(){ }); }; - var requestHandler = function(req, res){ + var requestHandler = function(req, res) { //Response may be shutdown when do the filter, in order not to cause exception, //Rewrite the write/writeHead functionalities of current response object var endFn = res.end; - res.end = function(){ + res.end = function() { //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object - res.write = res.writeHead = function(){ + res.write = res.writeHead = function() { console.log("response is already end, response.write ignored!") }; }; - res.writeFile = function(filePath, cb){ + res.writeFile = function(filePath, cb) { self.writeFile(res, filePath, cb); }; - res.redirect = function(url, status){ + res.redirect = function(url, status) { res.writeHead(status ? status : 302, { "Location": url }); res.end(); }; //Define filter object - req.filter = new FilterChain(function(){ + req.filter = new FilterChain(function() { //if handler not match, send the request !Handler.handle(req, res) && fileHandler(req, res); }); @@ -726,9 +729,9 @@ var WebSvr = (function(){ req.filter.next(req, res); }; - var writeFile = function(res, fullPath){ - fs.readFile(fullPath, function(err, data){ - if(err){ + var writeFile = function(res, fullPath) { + fs.readFile(fullPath, function(err, data) { + if (err) { console.log(err); return; } @@ -748,15 +751,15 @@ var WebSvr = (function(){ self.session = Handler.session; //Get a fullpath of a request - self.getFullPath = function(filePath){ + self.getFullPath = function(filePath) { return path.join(root, filePath); }; //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb){ + self.writeFile = function(res, filePath, cb) { filePath = path.join(root, filePath); - fs.exists(filePath, function(exist){ - if(exist){ + fs.exists(filePath, function(exist) { + if (exist) { writeFile(res, filePath); cb && cb(exist); }else{ @@ -770,18 +773,18 @@ var WebSvr = (function(){ //TODO: Support 304 client-side cache - self.write403 = function(res){ + self.write403 = function(res) { res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); }; - self.write404 = function(res){ + self.write404 = function(res) { res.writeHead(404, {"Content-Type": "text/html"}); res.end("File not found!"); }; //Public: start http server - self.start = function(){ + self.start = function() { //Update the default value of Settings options = _.extend({}, Settings, options); @@ -789,7 +792,7 @@ var WebSvr = (function(){ port = parseInt(options.port); //Create http server - if(options.http){ + if (options.http) { var httpSvr = require("http").createServer(requestHandler); httpSvr.listen(port); @@ -802,7 +805,7 @@ var WebSvr = (function(){ } //Create https server - if(options.https){ + if (options.https) { var httpsOpts = options.httpsOpts, httpsPort = options.httpsPort; @@ -816,10 +819,15 @@ var WebSvr = (function(){ self.httpsSvr = httpsSvr; } + + //diable console.log information + if (!options.debug) { + console.log = function(){}; + } }; //Public: close http server; - self.close = function(){ + self.close = function() { self.httpSvr && self.httpSvr.close(); self.httpsSvr && self.httpsSvr.close(); }; @@ -837,7 +845,8 @@ var webSvr = new WebSvr({ root: "../", listDir: true, https: true, - httpsPort: 8443 + httpsPort: 8443, + debug: true }); webSvr.start(); @@ -847,7 +856,7 @@ General filter: parse the post data / session before all request parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ -webSvr.filter(function(req, res){ +webSvr.filter(function(req, res) { //TODO: Add greeting words in filter //res.write("Hello WebSvr!
    "); @@ -858,9 +867,9 @@ webSvr.filter(function(req, res){ /* Session Filter: protect test/* folder => (validation by session); */ -webSvr.filter(/test\/[\w\.]+/, function(req, res){ +webSvr.filter(/test\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation - if(req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0){ + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { !req.session.get("username") && res.end("You must login, first!"); } @@ -874,15 +883,15 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.session("login.do", function(req, res){ +webSvr.session("login.do", function(req, res) { var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); var qs = querystring.parse(req.body); - if(qs.username == "admin" && qs.password == "12345678"){ + if (qs.username == "admin" && qs.password == "12345678") { //Put key/value pair in session //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session){ + req.session.set("username", qs.username, function(session) { //TODO: Add req.redirect / req.forward functionalities; res.writeHead(200, {"Content-Type": "text/html"}); res.writeFile("/test/setting.htm"); @@ -896,7 +905,7 @@ webSvr.session("login.do", function(req, res){ /* Uploader: upload.do => (receive handler) */ -webSvr.file("upload.do", function(req, res){ +webSvr.file("upload.do", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -907,7 +916,7 @@ webSvr.file("upload.do", function(req, res){ /* Redirect: redirect request, try at: http://localhost:8054/redirect */ -webSvr.url("redirect", function(req, res){ +webSvr.url("redirect", function(req, res) { res.redirect("/svr/websvr.all.js"); }); @@ -920,7 +929,7 @@ webSvr.url("combine", ["svr/tool/Combine.js"]); //Mapping "hello" to a string, trying at http://localhost:8054/hello webSvr.url("hello", "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post("post.htm", function(req, res){ +webSvr.post("post.htm", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //Witch session support: "{session: true}" res.write("You username is " + req.session.get("username")); diff --git a/svr/WebSvr.js b/svr/WebSvr.js index 40fe126..dad0a65 100644 --- a/svr/WebSvr.js +++ b/svr/WebSvr.js @@ -5,9 +5,9 @@ /* * WebSvr Namespace */ -var WebSvr = (function(){ +var WebSvr = (function() { - var server = function(options){ + var server = function(options) { //Library var fs = require("fs"), path = require("path"), @@ -19,7 +19,7 @@ var WebSvr = (function(){ root, port; - var fileHandler = function(req, res){ + var fileHandler = function(req, res) { var url = req.url, hasQuery = url.indexOf("?"); @@ -29,18 +29,18 @@ var WebSvr = (function(){ var fullPath = path.join(root, url); - fs.stat(fullPath, function(err, stat){ + fs.stat(fullPath, function(err, stat) { //Consider as file not found - if(err) return self.write404(res); + if (err) return self.write404(res); //Is file? Open this file and send to client. - if(stat.isFile()){ + if (stat.isFile()) { writeFile(res, fullPath); } //Is Directory? List all the files and folders. - else if(stat.isDirectory()){ + else if (stat.isDirectory()) { options.listDir ? ListDir.list(req, res, fullPath) : self.write403(res); @@ -49,30 +49,30 @@ var WebSvr = (function(){ }); }; - var requestHandler = function(req, res){ + var requestHandler = function(req, res) { //Response may be shutdown when do the filter, in order not to cause exception, //Rewrite the write/writeHead functionalities of current response object var endFn = res.end; - res.end = function(){ + res.end = function() { //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object - res.write = res.writeHead = function(){ + res.write = res.writeHead = function() { console.log("response is already end, response.write ignored!") }; }; - res.writeFile = function(filePath, cb){ + res.writeFile = function(filePath, cb) { self.writeFile(res, filePath, cb); }; - res.redirect = function(url, status){ + res.redirect = function(url, status) { res.writeHead(status ? status : 302, { "Location": url }); res.end(); }; //Define filter object - req.filter = new FilterChain(function(){ + req.filter = new FilterChain(function() { //if handler not match, send the request !Handler.handle(req, res) && fileHandler(req, res); }); @@ -81,9 +81,9 @@ var WebSvr = (function(){ req.filter.next(req, res); }; - var writeFile = function(res, fullPath){ - fs.readFile(fullPath, function(err, data){ - if(err){ + var writeFile = function(res, fullPath) { + fs.readFile(fullPath, function(err, data) { + if (err) { console.log(err); return; } @@ -103,15 +103,15 @@ var WebSvr = (function(){ self.session = Handler.session; //Get a fullpath of a request - self.getFullPath = function(filePath){ + self.getFullPath = function(filePath) { return path.join(root, filePath); }; //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb){ + self.writeFile = function(res, filePath, cb) { filePath = path.join(root, filePath); - fs.exists(filePath, function(exist){ - if(exist){ + fs.exists(filePath, function(exist) { + if (exist) { writeFile(res, filePath); cb && cb(exist); }else{ @@ -125,18 +125,18 @@ var WebSvr = (function(){ //TODO: Support 304 client-side cache - self.write403 = function(res){ + self.write403 = function(res) { res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); }; - self.write404 = function(res){ + self.write404 = function(res) { res.writeHead(404, {"Content-Type": "text/html"}); res.end("File not found!"); }; //Public: start http server - self.start = function(){ + self.start = function() { //Update the default value of Settings options = _.extend({}, Settings, options); @@ -144,7 +144,7 @@ var WebSvr = (function(){ port = parseInt(options.port); //Create http server - if(options.http){ + if (options.http) { var httpSvr = require("http").createServer(requestHandler); httpSvr.listen(port); @@ -157,7 +157,7 @@ var WebSvr = (function(){ } //Create https server - if(options.https){ + if (options.https) { var httpsOpts = options.httpsOpts, httpsPort = options.httpsPort; @@ -171,10 +171,15 @@ var WebSvr = (function(){ self.httpsSvr = httpsSvr; } + + //diable console.log information + if (!options.debug) { + console.log = function(){}; + } }; //Public: close http server; - self.close = function(){ + self.close = function() { self.httpSvr && self.httpSvr.close(); self.httpsSvr && self.httpsSvr.close(); }; diff --git a/svr/tool/Combine.js b/svr/tool/Combine.js index b3e052a..fba9156 100644 --- a/svr/tool/Combine.js +++ b/svr/tool/Combine.js @@ -7,7 +7,7 @@ //Combine namespace var Combine; -(function(){ +(function() { var fs = require("fs"), path = require("path"), @@ -21,10 +21,10 @@ var Combine; watch: false, //listen on the changes? //Interface - init: function(sourceFile, targetFile, watch){ + init: function(sourceFile, targetFile, watch) { //Combine type, it's a directory or cfg file - fs.stat(sourceFile, function(err, stat){ - if(err) { + fs.stat(sourceFile, function(err, stat) { + if (err) { console.log(err); return; } @@ -34,43 +34,42 @@ var Combine; combine.watch = watch; //It's a configuration files or dictionary - if(stat.isFile()){ + if (stat.isFile()) { combine.setCfg(sourceFile); - }else{ + } else { combine.setDir(sourceFile); } }); }, //get output stream - getStream: function(){ + getStream: function() { var stream; - try{ + try { stream = fs.createWriteStream(combine.targetFile); - } - catch(err){ + } catch (err) { console.log("Can't create output stream: ", err); } return stream; }, //get files from cfgFile, return absolute file path - getFiles: function(cfgPath){ + getFiles: function(cfgPath) { var contents = fs.readFileSync(cfgPath, 'utf-8'), files = [], lastIdx = cfgPath.lastIndexOf('\\'), dir = cfgPath.substring(0, lastIdx > -1 ? lastIdx : cfgPath.lastIndexOf('/') ); //read a file line-by-line - contents.match(/[^\r\n]+/g).forEach(function(line){ + contents.match(/[^\r\n]+/g).forEach(function(line) { //ignore comments that begin with '#' - if(line[0] != '#'){ + if (line[0] != '#') { files.push(path.join(dir, line)); } }); - combine.watch && files.forEach(function(file){ - if(combine.list.indexOf(file) < 0){ + combine.watch && files.forEach(function(file) { + if (combine.list.indexOf(file) < 0) { combine.watchFile(file); combine.list.push(file); }; @@ -82,18 +81,18 @@ var Combine; }, //Watch changes on source folder - setDir: function(directory){ + setDir: function(directory) { //Combine at the first running, then watching the changes. - if(combine.combineDir(directory)){ - combine.watch && fs.watch(directory, function(){ + if (combine.combineDir(directory)) { + combine.watch && fs.watch(directory, function() { combine.combineDir(directory); }); } }, //Watch chagnes on configuration fiel - setCfg: function(configuration){ - var combineCfg = function(){ + setCfg: function(configuration) { + var combineCfg = function() { //get file list from the configuration files. combine.getFiles(configuration); combine.combine(); @@ -107,29 +106,28 @@ var Combine; }, //Watch changes on a file - watchFile: function(file){ - try{ - fs.watch(file, function(){ + watchFile: function(file) { + try { + fs.watch(file, function() { combine.combine(); }); - } - catch(err){ + } catch (err) { console.log(file, err); } }, //Combine directory - combineDir: function(directory){ - try{ + combineDir: function(directory) { + try { var allFiles = fs.readdirSync(directory), //File name must be consist of numbers characters or "-" "_", "." fileReg = /^[a-zA-Z0-9-_\.]+$/, files = []; - allFiles.forEach(function(file){ - if(fileReg.test(file)){ + allFiles.forEach(function(file) { + if (fileReg.test(file)) { files.push(path.join(directory, file)); - }else{ + } else { console.log("Skip file:" + file); } }); @@ -137,37 +135,36 @@ var Combine; combine.files = files; return combine.combine(); - } - catch(err){ + } catch (err) { console.log(err); return false; } }, //Combine set of files into one - combine: function(){ + combine: function() { //Prevent other request within 1 seconds; - if(timerID) return; + if (timerID) return; - timerID = setTimeout(function(){ + timerID = setTimeout(function() { timerID = null; }, 1000); var oStream = combine.getStream(), r = true; - if(!oStream){ + if (!oStream) { return false; } var files = combine.files; - try{ - files.forEach(function(file){ + try { + files.forEach(function(file) { var stat = fs.statSync(file); - if(!stat.isFile()){ + if (!stat.isFile()) { console.log("Skip folder:" + file); - }else{ + } else { var data = fs.readFileSync(file); oStream.write("/*" + file + "*/\r\n"); oStream.write(data); @@ -184,8 +181,7 @@ var Combine; ", date:", new Date().toTimeString(), "\r\n\r\n" ); - } - catch(err){ + } catch (err) { console.log(err); r = false; } @@ -203,20 +199,20 @@ var Combine; * -o filepath: output files * -w: keep watch the changes? */ -(function(){ +(function() { /* * parsing parameters from command line * etc, node combine.js -i configfile.path -o outputfile.path * the parameter will be: '-' + one character, like: parsing('-o'); */ - var parsing = function(args, key){ - if(!key || key.length != 2 || key[0] != '-') return; + var parsing = function(args, key) { + if (!key || key.length != 2 || key[0] != '-') return; var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), param = args.match(reg); - if(param && param[0]){ + if (param && param[0]) { return param[0].substr(4, 500); } }; From f02e10c6ac4458933fa1cfed3c5321fb53632db5 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 6 Nov 2012 14:26:10 +0800 Subject: [PATCH 025/195] optimize formatting --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index fdbdc1a..22ac00b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ Session based authentication (session stored in files), all the request under "t parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ -<<<<<<< HEAD webSvr.filter(function(req, res){ //TODO: Add greeting words in filter //res.write("Hello WebSvr!
    "); @@ -49,10 +48,7 @@ Session based authentication (session stored in files), all the request under "t /* Session Filter: protect test/* folder => (validate by session); */ - webSvr.filter(/test\/[\w\.]+/, function(req, res){ -======= webSvr.filter(/test\/[\w\.]+/, function(req, res) { ->>>>>>> optimize //It's not index.htm/login.do, do the session validation if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { !req.session.get("username") && res.end("You must login, first!"); @@ -72,11 +68,7 @@ Handle Login and put the username in Session username: admin password: 12345678 */ -<<<<<<< HEAD - webSvr.session("login.do", function(req, res){ -======= webSvr.session(/login.do/, function(req, res) { ->>>>>>> optimize var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); @@ -102,11 +94,7 @@ Receive upload file (it's a specfic filter) /* Uploader: upload.do => (receive handler) */ -<<<<<<< HEAD - webSvr.file("upload.do", function(req, res){ -======= webSvr.file(/upload.do/, function(req, res) { ->>>>>>> optimize res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body From 2378b6867a07c53a000a0fb55f9b7ca518c948b3 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 8 Nov 2012 11:24:55 +0800 Subject: [PATCH 026/195] formatting codes formatting codes --- svr/websvr.js | 841 ++++++++++++++++ test/start.bat | 4 + test/svr/cert/certificate.pem | 17 + test/svr/cert/privatekey.pem | 15 + test/svr/sitetest.js | 115 +++ test/tmp/session/readme.txt | 1 + test/tmp/upload/readme.txt | 1 + test/web/index.htm | 31 + test/web/setting.htm | 17 + websvr/Filter.js | 74 ++ websvr/Global.js | 18 + websvr/Handler.js | 81 ++ websvr/ListDir.js | 97 ++ websvr/MakeFile.list | 20 + websvr/Mapper.js | 49 + websvr/Parser.js | 70 ++ websvr/RequestParser.js | 27 + svr/WebSvr.js => websvr/Server.js | 12 +- websvr/SessionParser.js | 91 ++ websvr/Settings.js | 31 + websvr/build.bat | 5 + websvr/lib/file.js | 73 ++ websvr/lib/incoming_form.js | 390 ++++++++ websvr/lib/mime.js | 93 ++ websvr/lib/multipart_parser.js | 312 ++++++ websvr/lib/querystring_parser.js | 25 + websvr/lib/types/mime.types | 1510 +++++++++++++++++++++++++++++ websvr/lib/types/node.types | 54 ++ websvr/lib/underscore.js | 1200 +++++++++++++++++++++++ websvr/lib/util.js | 6 + websvr/ref/Math.uuid.js | 92 ++ websvr/tool/Combine.js | 226 +++++ websvr/websvr.js | 838 ++++++++++++++++ 33 files changed, 6431 insertions(+), 5 deletions(-) create mode 100644 svr/websvr.js create mode 100644 test/start.bat create mode 100644 test/svr/cert/certificate.pem create mode 100644 test/svr/cert/privatekey.pem create mode 100644 test/svr/sitetest.js create mode 100644 test/tmp/session/readme.txt create mode 100644 test/tmp/upload/readme.txt create mode 100644 test/web/index.htm create mode 100644 test/web/setting.htm create mode 100644 websvr/Filter.js create mode 100644 websvr/Global.js create mode 100644 websvr/Handler.js create mode 100644 websvr/ListDir.js create mode 100644 websvr/MakeFile.list create mode 100644 websvr/Mapper.js create mode 100644 websvr/Parser.js create mode 100644 websvr/RequestParser.js rename svr/WebSvr.js => websvr/Server.js (91%) create mode 100644 websvr/SessionParser.js create mode 100644 websvr/Settings.js create mode 100644 websvr/build.bat create mode 100644 websvr/lib/file.js create mode 100644 websvr/lib/incoming_form.js create mode 100644 websvr/lib/mime.js create mode 100644 websvr/lib/multipart_parser.js create mode 100644 websvr/lib/querystring_parser.js create mode 100644 websvr/lib/types/mime.types create mode 100644 websvr/lib/types/node.types create mode 100644 websvr/lib/underscore.js create mode 100644 websvr/lib/util.js create mode 100644 websvr/ref/Math.uuid.js create mode 100644 websvr/tool/Combine.js create mode 100644 websvr/websvr.js diff --git a/svr/websvr.js b/svr/websvr.js new file mode 100644 index 0000000..f951480 --- /dev/null +++ b/svr/websvr.js @@ -0,0 +1,841 @@ +/*Global.js*/ +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +- Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +- Underscore: Add underscore a utility-belt library for JavaScript + https://github.com/documentcloud/underscore +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); +*/ +/*Settings.js*/ +/* +Configurations +*/ +var Settings = { + version: 0.022, + + //root folder of web + root: "../", + + //list files in directory + listDir: false, + + //http + http: true, + //default port of http + port: 8054, + + //enable debug information output? + debug: false, + + //https + https: false, + //default port of https + httpsPort: 8443, + httpsOpts: { + key: require("fs").readFileSync("cert/privatekey.pem"), + cert: require("fs").readFileSync("cert/certificate.pem") + }, + + //session file stored here, must be end with "/" + sessionDir: "../tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "../tmp/upload/" +}; + +/*ref\Math.uuid.js*/ +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); + +/*Mapper.js*/ +/* +Mapper: Used for Filter & Handler, +expression: required parameter +handler: required parameter +options: other optional parameters +*/ + +var Mapper = function(expression, handler, options) { + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); +}; + +Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req) { + var self = this, + expression = self.expression; + + //No expression? It's a general filter mapper + if (!expression) return true; + + switch(expression.constructor) { + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options) { + for(key in options) { + this[key] = options[key] + } + } +}; +/*RequestParser.js*/ +/* +Request parser, parse the data in request body via +when parse complete, execute the callback, with response data; +*/ +var RequestParser; + +(function() { + + //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE); + + RequestParser = function(req, res, callback) { + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + +}()); +/*SessionParser.js*/ + +var SessionParser; + +//TODO: Need a child process of clear session +(function() { + + var fs = require("fs"); + + SessionParser = (function(req, res, callback) { + + var sessionDir = Settings.sessionDir; + + var self = { + //session id + sid : null, + //session stored object + obj : {} + }; + + //TODO + self.set = function(key, val, callback) { + + var sessionfile = sessionDir + self.sid; + + key && (self.obj[key] = val); + + fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } + + callback && callback(self); + }); + }; + + //TO DO + self.get = function(key) { + return self.obj[key]; + }; + + self.init = function() { + var sidKey = "_wsid", + sidVal, + cookie = req.headers.cookie || ""; + + //Get or Create sid + var idx = cookie.indexOf(sidKey + "="); + + //sid exist in the cookie, read it + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //sid doesn't exist, create it; + if (idx < 0 || sidVal.length != 32) { + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + }; + self.sid = sidVal; + + //We only receive the cookie from Http headers + var sessionfile = sessionDir + self.sid; + + //here will be cause a bit of delay + fs.exists(sessionfile, function (exists) { + if (exists) { + fs.readFile( sessionfile, function (err, data) { + if (err) { + console.log(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + callback(self); + }); + }else{ + //session not exist create one + self.obj = {}; + self.set(null , null , callback); + } + }); + + }; + + self.init(); + + return self; + + }); + +}()); +/*Parser.js*/ +/* +Parser: Functions that Filter and Handler will be called +*/ +var Parser = function(req, res, mapper) { + + var handler = mapper.handler; + + //add sesion support + var parseSession = function() { + //add sesion support + if (mapper.session && typeof req.session == "undefined") { + SessionParser(req, res, function(session) { + req.session = session; + handler(req, res); + }); + }else{ + handler(req, res); + } + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseRequest = function() { + //need to parse the request? + if (mapper.parse && typeof req.body == "undefined") { + //Must parser the request first, or the post data will lost; + RequestParser(req, res, function(data) { + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function() { + //Need to parse the file in request? + if (mapper.file && typeof req.body == "undefined") { + //Must parser the request first, or the post data maybe lost; + var formidable = require('./lib/incoming_form'), + form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; + + form.parse(req, function(err, fields, files) { + if (err) { + console.log(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseRequest(); + }); + }else{ + parseRequest(); + }; + }; + + parseFile(); + +}; + +/*Filter.js*/ +/* +Http Filter: Execute all the rules that matched, +Filters will be always called before a handler. +*/ +var Filter = { + //filter list + filters: [], + + /* + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] + */ + filter: function(expression, handler, options) { + //The first parameter is Function => (handler, options) + if (expression.constructor == Function) { + options = handler; + handler = expression; + expression = null; + } + + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); + }, + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + file: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, {file: true}); + //insert as the first elements + Filter.filters.splice(0, 0, mapper); + } +}; + +/* +Filter Chain +*/ +var FilterChain = function(cb) { + var self = this; + self.idx = 0; + self.cb = cb; +}; + +FilterChain.prototype = { + next: function(req, res) { + var self = this; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if (!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res) { + //filter actions + req.next(req, res); + }, options); + */ + if (mapper.match(req)) { + + console.log("filter matched", self.idx, mapper.expression, req.url); + + Parser(req, res, mapper); + }else{ + self.next(req, res); + } + } +}; +/*Handler.js*/ +/* +Http Handler: Execute and returned when when first matched; +At the same time only one Handler will be called; +*/ +var Handler; + +(function() { + + /* + Private: handler list + */ + var handlers = []; + + /* + Static Handler instance + */ + Handler = { + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ + url: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, options); + handlers.push(mapper); + }, + + //Post: Parse the post data by default; + post: function(expression, handler, options) { + this.url(expression, handler, _.extend({ parse: true }, options)); + }, + + //Session: Parse the session and post by default; + session: function(expression, handler) { + this.url(expression, handler, { parse: true, session: true }); + }, + + handle: function(req, res) { + //flag: is matched? + for(var i = 0, len = handlers.length; i < len ; i++) { + + var mapper = handlers[i]; + if (mapper.match(req)) { + + console.log("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type) { + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + return true; + } + } + + return false; + + } //end of handle + + }; + +}()); + + + + +/*ListDir.js*/ +/* +ListDir: List all the files in a directory +*/ +var ListDir = (function() { + + var fs = require("fs"), + path = require("path"); + + var urlFormat = function(url) { + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date) { + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num) { + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url) { + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list: function(req, res, dir) { + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function() { + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function() {
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files) { + if (err) { + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++) { + //Persistent the idx before make the sync process + (function(idx) { + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat) { + cur++; + + if (err) { + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + +}()); +/*Server.js*/ +/* +* Description: Create a Web Server +* Author: Kris Zhang +* Licenses: MIT, GPL +*/ +/* +* Define WebSvr +* Export WebSvr +*/ +var WebSvr = module.exports = (function() { + + var server = function(options) { + //Library + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"); + + //Parameters + //Count: How many files? + var self = this, + root, + port; + + var fileHandler = function(req, res) { + + var url = req.url, + hasQuery = url.indexOf("?"); + + //fs.stat can't recognize the file name with querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; + + var fullPath = path.join(root, url); + + fs.stat(fullPath, function(err, stat) { + + //Consider as file not found + if (err) return self.write404(res); + + //Is file? Open this file and send to client. + if (stat.isFile()) { + writeFile(res, fullPath); + } + + //Is Directory? List all the files and folders. + else if (stat.isDirectory()) { + options.listDir + ? ListDir.list(req, res, fullPath) + : self.write403(res); + } + + }); + }; + + var requestHandler = function(req, res) { + //Response may be shutdown when do the filter, in order not to cause exception, + //Rewrite the write/writeHead functionalities of current response object + var endFn = res.end; + res.end = function() { + //Execute old end + endFn.apply(res, arguments); + //Rewirte write/writeHead on response object + res.write = res.writeHead = function() { + console.log("response is already end, response.write ignored!") + }; + }; + + res.writeFile = function(filePath, cb) { + self.writeFile(res, filePath, cb); + }; + + res.redirect = function(url, status) { + res.writeHead(status ? status : 302, { "Location": url }); + res.end(); + }; + + //Define filter object + req.filter = new FilterChain(function() { + //if handler not match, send the request + !Handler.handle(req, res) && fileHandler(req, res); + }); + + //Handle the first filter + req.filter.next(req, res); + }; + + var writeFile = function(res, fullPath) { + fs.readFile(fullPath, function(err, data) { + if (err) { + console.log(err); + return; + } + res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + res.end(data, "binary"); + }); + }; + + //Explose API + //Filter + self.filter = Filter.filter; + self.file = Filter.file; + + //Handler + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; + + //Get a fullpath of a request + self.getFullPath = function(filePath) { + return path.join(root, filePath); + }; + + //Write file, filePath is relative path + self.writeFile = function(res, filePath, cb) { + filePath = path.join(root, filePath); + fs.exists(filePath, function(exist) { + if (exist) { + writeFile(res, filePath); + cb && cb(exist); + }else{ + //If callback function doesn't exist, write 404 page; + cb ? cb(exist) : self.write404(res); + } + }); + }; + + //TODO: Support 301 move permanently + + //TODO: Support 304 client-side cache + + self.write403 = function(res) { + res.writeHead(403, {"Content-Type": "text/html"}); + res.end("Access forbidden!"); + }; + + self.write404 = function(res) { + res.writeHead(404, {"Content-Type": "text/html"}); + res.end("File not found!"); + }; + + //Public: start http server + self.start = function() { + //Update the default value of Settings + options = _.extend({}, Settings, options); + + root = options.root; + port = parseInt(options.port); + + //Create http server + if (options.http) { + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Http server running at" + ,"Root:", root + ,"Port:", port + ); + + self.httpSvr = httpSvr; + } + + //Create https server + if (options.https) { + var httpsOpts = options.httpsOpts, + httpsPort = options.httpsPort; + + var httpsSvr = require("https").createServer(httpsOpts, requestHandler); + httpsSvr.listen(httpsPort); + + console.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); + + self.httpsSvr = httpsSvr; + } + + //diable console.log information + if (!options.debug) { + console.log = function(){}; + } + }; + + //Public: close http server; + self.close = function() { + self.httpSvr && self.httpSvr.close(); + self.httpsSvr && self.httpsSvr.close(); + }; + + }; + + return server; + +})(); diff --git a/test/start.bat b/test/start.bat new file mode 100644 index 0000000..45982a7 --- /dev/null +++ b/test/start.bat @@ -0,0 +1,4 @@ +REM start sitetest +node svr/sitetest.js + +pause; \ No newline at end of file diff --git a/test/svr/cert/certificate.pem b/test/svr/cert/certificate.pem new file mode 100644 index 0000000..f660f7d --- /dev/null +++ b/test/svr/cert/certificate.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICpTCCAg4CCQCISrlY6+bq+DANBgkqhkiG9w0BAQUFADCBljELMAkGA1UEBhMC +Q04xETAPBgNVBAgMCFNoYW5naGFpMREwDwYDVQQHDAhTaGFuZ2hhaTEQMA4GA1UE +CgwHRW5sb2dpYzEfMB0GA1UECwwWUmVzZWFyY2ggJiBEZXZlbG9wbWVudDESMBAG +A1UEAwwJbG9jYWxob3N0MRowGAYJKoZIhvcNAQkBFgtjMnVAbGl2ZS5jbjAeFw0x +MjEwMjgwNzU1MTZaFw0xMjExMjcwNzU1MTZaMIGWMQswCQYDVQQGEwJDTjERMA8G +A1UECAwIU2hhbmdoYWkxETAPBgNVBAcMCFNoYW5naGFpMRAwDgYDVQQKDAdFbmxv +Z2ljMR8wHQYDVQQLDBZSZXNlYXJjaCAmIERldmVsb3BtZW50MRIwEAYDVQQDDAls +b2NhbGhvc3QxGjAYBgkqhkiG9w0BCQEWC2MydUBsaXZlLmNuMIGfMA0GCSqGSIb3 +DQEBAQUAA4GNADCBiQKBgQCgWDgMTdIEafBqczxi4BpFOL741bf3nttkJxS5eMbj +bQvfDX9LHkNpqZFx3jDWecmjBhUY+B2Q1PmiBnwuRLIBaYsg3jT2nWp0fphzBcJ5 +JsPF3S9GjeWrzYZIuK+YRyCarlmzbjn4omidm/4wAtXuXxQYiouTMe9hyuLnxb6s +mQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAB/dirZrcdB4aGUTlSGWt+LmFuz/Xl6x +iWkfjwvQP7h4oCGoSUjoB9vqy5hIkCKxI+PWhW76S7YpJNXHIdjrC7BRyYVIlAmp +Efm4GUlBAdSYo222my1xTF8SaEx1debDfzq2Ho8xeozpnKae4x62MoA+Fp2kphsq +tLTB8YxbjIQy +-----END CERTIFICATE----- diff --git a/test/svr/cert/privatekey.pem b/test/svr/cert/privatekey.pem new file mode 100644 index 0000000..5f7786a --- /dev/null +++ b/test/svr/cert/privatekey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCgWDgMTdIEafBqczxi4BpFOL741bf3nttkJxS5eMbjbQvfDX9L +HkNpqZFx3jDWecmjBhUY+B2Q1PmiBnwuRLIBaYsg3jT2nWp0fphzBcJ5JsPF3S9G +jeWrzYZIuK+YRyCarlmzbjn4omidm/4wAtXuXxQYiouTMe9hyuLnxb6smQIDAQAB +AoGAWDpSlMqZPh6A0EIaPxmqut4PjuIiORlrBL/QUoHXhjpxZsmJem7rjw9j3XDy +FIGs5owpPbUAp7nYpkPFPrxD6U3qVwZk/C7K2Lx2vWYlJ9vf8TTWAvIt6yyNuq5x +ocFosz346viky3K5QkyvmT9y3UuQS65GKj8Hv5S9xSolAhUCQQDSGzabd9c1kgex +EH96Fu/EWIE2G2U3f5/MRalAPlCIrVkBo1YjNoO8QDIb61mW0KdunQrtxrOoKDJp +Opxwpkj7AkEAw15qkHWd2yfU4yw7cFWsWOLojs3pyLdSX+7++vT3gLL8y2otbyKi +mWlrQJDgCnbXywUyRJtMGmLrn9X0y5IUewJBAJxGlYVpy+8SoRn4dXjwGoLmeaUv +F0gCa29a2RrpvqkKlst7HBSw9adN8HeHxGlC5WaG9JwLUZHf5C8U40t+w4UCQClz +keaeneSO2fNtQhs+gjfFxRPviofEpZynJ8B1U0IiN9Ks74Dh91/XZyMm2fI+buCr +dJPr40TB8j5SdgLvNpsCQFaYxoFde72QZHBQKYMsOmKldhcSuQKeZ/oppuhjgYqp +9qrtIyQ2MTchc8Yu27WCU975F0UGWQTsNG0BqVtGLik= +-----END RSA PRIVATE KEY----- diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js new file mode 100644 index 0000000..9afc04f --- /dev/null +++ b/test/svr/sitetest.js @@ -0,0 +1,115 @@ +//import namespace +var WebSvr = require("./../../websvr/websvr.js"); + +//Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; +//Trying at: http://localhost:8054 +var webSvr = new WebSvr({ + root: "./", + + //enable https + https: true, + //default port of https + httpsPort: 8443, + httpsOpts: { + key: require("fs").readFileSync("svr/cert/privatekey.pem"), + cert: require("fs").readFileSync("svr/cert/certificate.pem") + }, + + //Change the default locations of tmp session and upload files + //session file stored here, must be end with "/" + sessionDir: "tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "tmp/upload/", + + + listDir: true, + debug: true +}); + +webSvr.start(); + +/* +General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; + session: init the session and stored in req.session; +*/ +webSvr.filter(function(req, res) { + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(req, res); +}, {parse:true, session:true}); + +/* +Session Filter: protect test/* folder => (validation by session); +*/ +webSvr.filter(/web\/[\w\.]+/, function(req, res) { + //It's not index.htm/login.do, do the session validation + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { + !req.session.get("username") && res.end("You must login, first!"); + } + + //Link to next filter + req.filter.next(req, res); +}); + + +/* +Handler: login.do => (validate the username & password) + username: admin + password: 12345678 +*/ +webSvr.session("login.do", function(req, res) { + var querystring = require("querystring"); + + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if (qs.username == "admin" && qs.password == "12345678") { + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session) { + //TODO: Add req.redirect / req.forward functionalities; + res.writeHead(200, {"Content-Type": "text/html"}); + res.writeFile("/test/setting.htm"); + }); + }else{ + res.writeHead(401); + res.end("Wrong username/password"); + } +}); + +/* +Uploader: upload.do => (receive handler) +*/ +webSvr.file("upload.do", function(req, res) { + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); +}); + +/* +Redirect: redirect request, try at: http://localhost:8054/redirect +*/ +webSvr.url("redirect", function(req, res) { + res.redirect("/svr/websvr.all.js"); +}); + + +/* +Simple redirect API: +*/ +//Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine +webSvr.url("combine", ["svr/tool/Combine.js"]); +//Mapping "hello" to a string, trying at http://localhost:8054/hello +webSvr.url("hello", "Hello WebSvr!"); +//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm +webSvr.post("post.htm", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //Witch session support: "{session: true}" + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); +}, {session: true}); \ No newline at end of file diff --git a/test/tmp/session/readme.txt b/test/tmp/session/readme.txt new file mode 100644 index 0000000..0b4b98a --- /dev/null +++ b/test/tmp/session/readme.txt @@ -0,0 +1 @@ +Temporary session here. \ No newline at end of file diff --git a/test/tmp/upload/readme.txt b/test/tmp/upload/readme.txt new file mode 100644 index 0000000..9cf689f --- /dev/null +++ b/test/tmp/upload/readme.txt @@ -0,0 +1 @@ +Temporary upload files here. \ No newline at end of file diff --git a/test/web/index.htm b/test/web/index.htm new file mode 100644 index 0000000..6cfc301 --- /dev/null +++ b/test/web/index.htm @@ -0,0 +1,31 @@ + + + + + + Login + + + +

    WebSvr configuration login:

    +
    +
      +
    • + + + admin +
    • +
    • + + + 12345678 +
    • +
    • + + +
    • +
    +
    + + + diff --git a/test/web/setting.htm b/test/web/setting.htm new file mode 100644 index 0000000..39ed103 --- /dev/null +++ b/test/web/setting.htm @@ -0,0 +1,17 @@ + + + + + + Login + + +
    +
    + + +
    +
    + + + diff --git a/websvr/Filter.js b/websvr/Filter.js new file mode 100644 index 0000000..3497163 --- /dev/null +++ b/websvr/Filter.js @@ -0,0 +1,74 @@ +/* +Http Filter: Execute all the rules that matched, +Filters will be always called before a handler. +*/ +var Filter = { + //filter list + filters: [], + + /* + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] + */ + filter: function(expression, handler, options) { + //The first parameter is Function => (handler, options) + if (expression.constructor == Function) { + options = handler; + handler = expression; + expression = null; + } + + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); + }, + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + file: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, {file: true}); + //insert as the first elements + Filter.filters.splice(0, 0, mapper); + } +}; + +/* +Filter Chain +*/ +var FilterChain = function(cb) { + var self = this; + self.idx = 0; + self.cb = cb; +}; + +FilterChain.prototype = { + next: function(req, res) { + var self = this; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if (!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res) { + //filter actions + req.next(req, res); + }, options); + */ + if (mapper.match(req)) { + + console.log("filter matched", self.idx, mapper.expression, req.url); + + Parser(req, res, mapper); + }else{ + self.next(req, res); + } + } +}; \ No newline at end of file diff --git a/websvr/Global.js b/websvr/Global.js new file mode 100644 index 0000000..77499b3 --- /dev/null +++ b/websvr/Global.js @@ -0,0 +1,18 @@ +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +- Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +- Underscore: Add underscore a utility-belt library for JavaScript + https://github.com/documentcloud/underscore +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); +*/ \ No newline at end of file diff --git a/websvr/Handler.js b/websvr/Handler.js new file mode 100644 index 0000000..eda6d19 --- /dev/null +++ b/websvr/Handler.js @@ -0,0 +1,81 @@ +/* +Http Handler: Execute and returned when when first matched; +At the same time only one Handler will be called; +*/ +var Handler; + +(function() { + + /* + Private: handler list + */ + var handlers = []; + + /* + Static Handler instance + */ + Handler = { + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ + url: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, options); + handlers.push(mapper); + }, + + //Post: Parse the post data by default; + post: function(expression, handler, options) { + this.url(expression, handler, _.extend({ parse: true }, options)); + }, + + //Session: Parse the session and post by default; + session: function(expression, handler) { + this.url(expression, handler, { parse: true, session: true }); + }, + + handle: function(req, res) { + //flag: is matched? + for(var i = 0, len = handlers.length; i < len ; i++) { + + var mapper = handlers[i]; + if (mapper.match(req)) { + + console.log("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type) { + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + return true; + } + } + + return false; + + } //end of handle + + }; + +}()); + + + diff --git a/websvr/ListDir.js b/websvr/ListDir.js new file mode 100644 index 0000000..5d773f0 --- /dev/null +++ b/websvr/ListDir.js @@ -0,0 +1,97 @@ +/* +ListDir: List all the files in a directory +*/ +var ListDir = (function() { + + var fs = require("fs"), + path = require("path"); + + var urlFormat = function(url) { + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date) { + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num) { + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url) { + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list: function(req, res, dir) { + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function() { + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function() {
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files) { + if (err) { + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++) { + //Persistent the idx before make the sync process + (function(idx) { + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat) { + cur++; + + if (err) { + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + +}()); \ No newline at end of file diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list new file mode 100644 index 0000000..e22fdd7 --- /dev/null +++ b/websvr/MakeFile.list @@ -0,0 +1,20 @@ +#Config file +#These modules will be crunched to one. +#TODO: Need to support dictionary in the list; + +#Global object and description +Global.js +Settings.js + +#Reference: JavaScript library +ref/Math.uuid.js + +#WebSvr Modules +Mapper.js +RequestParser.js +SessionParser.js +Parser.js +Filter.js +Handler.js +ListDir.js +Server.js \ No newline at end of file diff --git a/websvr/Mapper.js b/websvr/Mapper.js new file mode 100644 index 0000000..2f31181 --- /dev/null +++ b/websvr/Mapper.js @@ -0,0 +1,49 @@ +/* +Mapper: Used for Filter & Handler, +expression: required parameter +handler: required parameter +options: other optional parameters +*/ + +var Mapper = function(expression, handler, options) { + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); +}; + +Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req) { + var self = this, + expression = self.expression; + + //No expression? It's a general filter mapper + if (!expression) return true; + + switch(expression.constructor) { + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options) { + for(key in options) { + this[key] = options[key] + } + } +}; \ No newline at end of file diff --git a/websvr/Parser.js b/websvr/Parser.js new file mode 100644 index 0000000..33e8753 --- /dev/null +++ b/websvr/Parser.js @@ -0,0 +1,70 @@ +/* +Parser: Functions that Filter and Handler will be called +*/ +var Parser = function(req, res, mapper) { + + var handler = mapper.handler; + + //add sesion support + var parseSession = function() { + //add sesion support + if (mapper.session && typeof req.session == "undefined") { + SessionParser(req, res, function(session) { + req.session = session; + handler(req, res); + }); + }else{ + handler(req, res); + } + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseRequest = function() { + //need to parse the request? + if (mapper.parse && typeof req.body == "undefined") { + //Must parser the request first, or the post data will lost; + RequestParser(req, res, function(data) { + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function() { + //Need to parse the file in request? + if (mapper.file && typeof req.body == "undefined") { + //Must parser the request first, or the post data maybe lost; + var formidable = require('./lib/incoming_form'), + form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; + + form.parse(req, function(err, fields, files) { + if (err) { + console.log(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseRequest(); + }); + }else{ + parseRequest(); + }; + }; + + parseFile(); + +}; diff --git a/websvr/RequestParser.js b/websvr/RequestParser.js new file mode 100644 index 0000000..2794277 --- /dev/null +++ b/websvr/RequestParser.js @@ -0,0 +1,27 @@ +/* +Request parser, parse the data in request body via +when parse complete, execute the callback, with response data; +*/ +var RequestParser; + +(function() { + + //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE); + + RequestParser = function(req, res, callback) { + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + +}()); \ No newline at end of file diff --git a/svr/WebSvr.js b/websvr/Server.js similarity index 91% rename from svr/WebSvr.js rename to websvr/Server.js index dad0a65..fb8f343 100644 --- a/svr/WebSvr.js +++ b/websvr/Server.js @@ -1,11 +1,13 @@ /* -* Description: Create a Web Server (http based). +* Description: Create a Web Server * Author: Kris Zhang +* Licenses: MIT, GPL */ /* -* WebSvr Namespace +* Define WebSvr +* Export WebSvr */ -var WebSvr = (function() { +var WebSvr = module.exports = (function() { var server = function(options) { //Library @@ -24,7 +26,7 @@ var WebSvr = (function() { var url = req.url, hasQuery = url.indexOf("?"); - //Bug: path.join can't recognize the querystring; + //fs.stat can't recognize the file name with querystring; url = hasQuery > 0 ? url.substring(0, hasQuery) : url; var fullPath = path.join(root, url); @@ -138,7 +140,7 @@ var WebSvr = (function() { //Public: start http server self.start = function() { //Update the default value of Settings - options = _.extend({}, Settings, options); + options = _.extend(Settings, options); root = options.root; port = parseInt(options.port); diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js new file mode 100644 index 0000000..4ece4e6 --- /dev/null +++ b/websvr/SessionParser.js @@ -0,0 +1,91 @@ + +var SessionParser; + +//TODO: Need a child process of clear session +(function() { + + var fs = require("fs"); + + SessionParser = (function(req, res, callback) { + + var sessionDir = Settings.sessionDir; + + var self = { + //session id + sid : null, + //session stored object + obj : {} + }; + + //TODO + self.set = function(key, val, callback) { + + var sessionfile = sessionDir + self.sid; + + key && (self.obj[key] = val); + + fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } + + callback && callback(self); + }); + }; + + //TO DO + self.get = function(key) { + return self.obj[key]; + }; + + self.init = function() { + var sidKey = "_wsid", + sidVal, + cookie = req.headers.cookie || ""; + + //Get or Create sid + var idx = cookie.indexOf(sidKey + "="); + + //sid exist in the cookie, read it + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //sid doesn't exist, create it; + if (idx < 0 || sidVal.length != 32) { + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + }; + self.sid = sidVal; + + //We only receive the cookie from Http headers + var sessionfile = sessionDir + self.sid; + + //here will be cause a bit of delay + fs.exists(sessionfile, function (exists) { + if (exists) { + fs.readFile( sessionfile, function (err, data) { + if (err) { + console.log(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + callback(self); + }); + }else{ + //session not exist create one + self.obj = {}; + self.set(null , null , callback); + } + }); + + }; + + self.init(); + + return self; + + }); + +}()); \ No newline at end of file diff --git a/websvr/Settings.js b/websvr/Settings.js new file mode 100644 index 0000000..272cc0c --- /dev/null +++ b/websvr/Settings.js @@ -0,0 +1,31 @@ +/* +Configurations +*/ +var Settings = { + version: 0.022, + + //root folder of web + root: "../", + + //list files in directory + listDir: false, + + //http + http: true, + //default port of http + port: 8054, + + //enable debug information output? + debug: false, + + //https + https: false, + //default port of https + httpsPort: 8443, + httpsOpts: { key:"", cert:"" }, + + //session file stored here, must be end with "/" + sessionDir: "../tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "../tmp/upload/" +}; diff --git a/websvr/build.bat b/websvr/build.bat new file mode 100644 index 0000000..264ea42 --- /dev/null +++ b/websvr/build.bat @@ -0,0 +1,5 @@ +REM Build websvr JS, and keep watching the changes +node tool/Combine.js -i makefile.list -o websvr.js + +REM Combine complete, Goodbye. +pause; \ No newline at end of file diff --git a/websvr/lib/file.js b/websvr/lib/file.js new file mode 100644 index 0000000..362019c --- /dev/null +++ b/websvr/lib/file.js @@ -0,0 +1,73 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +var util = require('./util'), + WriteStream = require('fs').WriteStream, + EventEmitter = require('events').EventEmitter, + crypto = require('crypto'); + +function File(properties) { + EventEmitter.call(this); + + this.size = 0; + this.path = null; + this.name = null; + this.type = null; + this.hash = null; + this.lastModifiedDate = null; + + this._writeStream = null; + + for (var key in properties) { + this[key] = properties[key]; + } + + if(typeof this.hash === 'string') { + this.hash = crypto.createHash(properties.hash); + } + + this._backwardsCompatibility(); +} +module.exports = File; +util.inherits(File, EventEmitter); + +// @todo Next release: Show error messages when accessing these +File.prototype._backwardsCompatibility = function() { + var self = this; + this.__defineGetter__('length', function() { + return self.size; + }); + this.__defineGetter__('filename', function() { + return self.name; + }); + this.__defineGetter__('mime', function() { + return self.type; + }); +}; + +File.prototype.open = function() { + this._writeStream = new WriteStream(this.path); +}; + +File.prototype.write = function(buffer, cb) { + var self = this; + this._writeStream.write(buffer, function() { + if(self.hash) { + self.hash.update(buffer); + } + self.lastModifiedDate = new Date(); + self.size += buffer.length; + self.emit('progress', self.size); + cb(); + }); +}; + +File.prototype.end = function(cb) { + var self = this; + this._writeStream.end(function() { + if(self.hash) { + self.hash = self.hash.digest('hex'); + } + self.emit('end'); + cb(); + }); +}; diff --git a/websvr/lib/incoming_form.js b/websvr/lib/incoming_form.js new file mode 100644 index 0000000..76eb944 --- /dev/null +++ b/websvr/lib/incoming_form.js @@ -0,0 +1,390 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +var fs = require('fs'); +var util = require('./util'), + path = require('path'), + File = require('./file'), + MultipartParser = require('./multipart_parser').MultipartParser, + QuerystringParser = require('./querystring_parser').QuerystringParser, + StringDecoder = require('string_decoder').StringDecoder, + EventEmitter = require('events').EventEmitter, + Stream = require('stream').Stream; + +function IncomingForm(opts) { + if (!(this instanceof IncomingForm)) return new IncomingForm; + EventEmitter.call(this); + + opts=opts||{}; + + this.error = null; + this.ended = false; + + this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; + this.keepExtensions = opts.keepExtensions || false; + this.uploadDir = opts.uploadDir || IncomingForm.UPLOAD_DIR; + this.encoding = opts.encoding || 'utf-8'; + this.headers = null; + this.type = null; + this.hash = false; + + this.bytesReceived = null; + this.bytesExpected = null; + + this._parser = null; + this._flushing = 0; + this._fieldsSize = 0; +}; +util.inherits(IncomingForm, EventEmitter); +exports.IncomingForm = IncomingForm; + +IncomingForm.UPLOAD_DIR = (function() { + var dirs = [ + process.env.TMP, + process.env.TMPDIR, + process.env.TEMP, + '/tmp', + process.cwd() + ]; + for (var i = 0; i < dirs.length; i++) { + var dir = dirs[i]; + var isDirectory = false; + + try { + isDirectory = fs.statSync(dir).isDirectory(); + } catch (e) {} + + if (isDirectory) return dir; + } +})(); + +IncomingForm.prototype.parse = function(req, cb) { + this.pause = function() { + try { + req.pause(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + return true; + }; + + this.resume = function() { + try { + req.resume(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + + return true; + }; + + this.writeHeaders(req.headers); + + var self = this; + req + .on('error', function(err) { + self._error(err); + }) + .on('aborted', function() { + self.emit('aborted'); + }) + .on('data', function(buffer) { + self.write(buffer); + }) + .on('end', function() { + if (self.error) { + return; + } + + var err = self._parser.end(); + if (err) { + self._error(err); + } + }); + + if (cb) { + var fields = {}, files = {}; + this + .on('field', function(name, value) { + fields[name] = value; + }) + .on('file', function(name, file) { + files[name] = file; + }) + .on('error', function(err) { + cb(err, fields, files); + }) + .on('end', function() { + cb(null, fields, files); + }); + } + + return this; +}; + +IncomingForm.prototype.writeHeaders = function(headers) { + this.headers = headers; + this._parseContentLength(); + this._parseContentType(); +}; + +IncomingForm.prototype.write = function(buffer) { + if (!this._parser) { + this._error(new Error('unintialized parser')); + return; + } + + this.bytesReceived += buffer.length; + this.emit('progress', this.bytesReceived, this.bytesExpected); + + var bytesParsed = this._parser.write(buffer); + if (bytesParsed !== buffer.length) { + this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); + } + + return bytesParsed; +}; + +IncomingForm.prototype.pause = function() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; +}; + +IncomingForm.prototype.resume = function() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; +}; + +IncomingForm.prototype.onPart = function(part) { + // this method can be overwritten by the user + this.handlePart(part); +}; + +IncomingForm.prototype.handlePart = function(part) { + var self = this; + + if (part.filename === undefined) { + var value = '' + , decoder = new StringDecoder(this.encoding); + + part.on('data', function(buffer) { + self._fieldsSize += buffer.length; + if (self._fieldsSize > self.maxFieldsSize) { + self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); + return; + } + value += decoder.write(buffer); + }); + + part.on('end', function() { + self.emit('field', part.name, value); + }); + return; + } + + this._flushing++; + + var file = new File({ + path: this._uploadPath(part.filename), + name: part.filename, + type: part.mime, + hash: self.hash + }); + + this.emit('fileBegin', part.name, file); + + file.open(); + + part.on('data', function(buffer) { + self.pause(); + file.write(buffer, function() { + self.resume(); + }); + }); + + part.on('end', function() { + file.end(function() { + self._flushing--; + self.emit('file', part.name, file); + self._maybeEnd(); + }); + }); +}; + +IncomingForm.prototype._parseContentType = function() { + if (!this.headers['content-type']) { + this._error(new Error('bad content-type header, no content-type')); + return; + } + + if (this.headers['content-type'].match(/urlencoded/i)) { + this._initUrlencoded(); + return; + } + + if (this.headers['content-type'].match(/multipart/i)) { + var m; + if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { + this._initMultipart(m[1] || m[2]); + } else { + this._error(new Error('bad content-type header, no multipart boundary')); + } + return; + } + + this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); +}; + +IncomingForm.prototype._error = function(err) { + if (this.error) { + return; + } + + this.error = err; + this.pause(); + this.emit('error', err); +}; + +IncomingForm.prototype._parseContentLength = function() { + if (this.headers['content-length']) { + this.bytesReceived = 0; + this.bytesExpected = parseInt(this.headers['content-length'], 10); + this.emit('progress', this.bytesReceived, this.bytesExpected); + } +}; + +IncomingForm.prototype._newParser = function() { + return new MultipartParser(); +}; + +IncomingForm.prototype._initMultipart = function(boundary) { + this.type = 'multipart'; + + var parser = new MultipartParser(), + self = this, + headerField, + headerValue, + part; + + parser.initWithBoundary(boundary); + + parser.onPartBegin = function() { + part = new Stream(); + part.readable = true; + part.headers = {}; + part.name = null; + part.filename = null; + part.mime = null; + headerField = ''; + headerValue = ''; + }; + + parser.onHeaderField = function(b, start, end) { + headerField += b.toString(self.encoding, start, end); + }; + + parser.onHeaderValue = function(b, start, end) { + headerValue += b.toString(self.encoding, start, end); + }; + + parser.onHeaderEnd = function() { + headerField = headerField.toLowerCase(); + part.headers[headerField] = headerValue; + + var m; + if (headerField == 'content-disposition') { + if (m = headerValue.match(/name="([^"]+)"/i)) { + part.name = m[1]; + } + + part.filename = self._fileName(headerValue); + } else if (headerField == 'content-type') { + part.mime = headerValue; + } + + headerField = ''; + headerValue = ''; + }; + + parser.onHeadersEnd = function() { + self.onPart(part); + }; + + parser.onPartData = function(b, start, end) { + part.emit('data', b.slice(start, end)); + }; + + parser.onPartEnd = function() { + part.emit('end'); + }; + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._fileName = function(headerValue) { + var m = headerValue.match(/filename="(.*?)"($|; )/i) + if (!m) return; + + var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); + filename = filename.replace(/%22/g, '"'); + filename = filename.replace(/&#([\d]{4});/g, function(m, code) { + return String.fromCharCode(code); + }); + return filename; +}; + +IncomingForm.prototype._initUrlencoded = function() { + this.type = 'urlencoded'; + + var parser = new QuerystringParser() + , self = this; + + parser.onField = function(key, val) { + self.emit('field', key, val); + }; + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._uploadPath = function(filename) { + var name = ''; + for (var i = 0; i < 32; i++) { + name += Math.floor(Math.random() * 16).toString(16); + } + + if (this.keepExtensions) { + var ext = path.extname(filename); + ext = ext.replace(/(\.[a-z0-9]+).*/, '$1') + + name += ext; + } + + return path.join(this.uploadDir, name); +}; + +IncomingForm.prototype._maybeEnd = function() { + if (!this.ended || this._flushing) { + return; + } + + this.emit('end'); +}; diff --git a/websvr/lib/mime.js b/websvr/lib/mime.js new file mode 100644 index 0000000..28dcbfe --- /dev/null +++ b/websvr/lib/mime.js @@ -0,0 +1,93 @@ +var path = require('path'), + fs = require('fs'); + +var mime = module.exports = { + // Map of extension to mime type + types: Object.create(null), + + // Map of mime type to extension + extensions :Object.create(null), + + /** + * Define mimetype -> extension mappings. Each key is a mime-type that maps + * to an array of extensions associated with the type. The first extension is + * used as the default extension for the type. + * + * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); + * + * @param map (Object) type definitions + */ + define: function(map) { + for (var type in map) { + var exts = map[type]; + + for (var i = 0; i < exts.length; i++) { + mime.types[exts[i]] = type; + } + + // Default extension is the first one we encounter + if (!mime.extensions[type]) { + mime.extensions[type] = exts[0]; + } + } + }, + + /** + * Load an Apache2-style ".types" file + * + * This may be called multiple times (it's expected). Where files declare + * overlapping types/extensions, the last file wins. + * + * @param file (String) path of file to load. + */ + load: function(file) { + // Read file and split into lines + var map = {}, + content = fs.readFileSync(file, 'ascii'), + lines = content.split(/[\r\n]+/); + + lines.forEach(function(line, lineno) { + // Clean up whitespace/comments, and split into fields + var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); + map[fields.shift()] = fields; + }); + + mime.define(map); + }, + + /** + * Lookup a mime type based on extension + */ + lookup: function(path, fallback) { + var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); + + return mime.types[ext] || fallback || mime.default_type + }, + + /** + * Return file extension associated with a mime type + */ + extension: function(mimeType) { + return mime.extensions[mimeType]; + }, + + /** + * Lookup a charset based on mime type. + */ + charsets: { + lookup: function (mimeType, fallback) { + // Assume text types are utf8. Modify mime logic as needed. + return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; + } + } +}; + +// Load our local copy of +// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types +mime.load(path.join(__dirname, 'types/mime.types')); + +// Overlay enhancements submitted by the node.js community +mime.load(path.join(__dirname, 'types/node.types')); + +// Set the default type +mime.default_type = mime.types.bin; diff --git a/websvr/lib/multipart_parser.js b/websvr/lib/multipart_parser.js new file mode 100644 index 0000000..f73140c --- /dev/null +++ b/websvr/lib/multipart_parser.js @@ -0,0 +1,312 @@ +var Buffer = require('buffer').Buffer, + s = 0, + S = + { PARSER_UNINITIALIZED: 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, + Z = 122, + + lower = function(c) { + return c | 0x20; + }; + +for (var s in S) { + exports[s] = S[s]; +} + +function MultipartParser() { + this.boundary = null; + this.boundaryChars = null; + this.lookbehind = null; + this.state = S.PARSER_UNINITIALIZED; + + this.index = null; + this.flags = 0; +}; +exports.MultipartParser = MultipartParser; + +MultipartParser.stateToString = function(stateNumber) { + for (var state in S) { + var number = S[state]; + if (number === stateNumber) return state; + } +}; + +MultipartParser.prototype.initWithBoundary = function(str) { + this.boundary = new Buffer(str.length+4); + this.boundary.write('\r\n--', 'ascii', 0); + this.boundary.write(str, 'ascii', 4); + this.lookbehind = new Buffer(this.boundary.length+8); + this.state = S.START; + + this.boundaryChars = {}; + for (var i = 0; i < this.boundary.length; i++) { + this.boundaryChars[this.boundary[i]] = true; + } +}; + +MultipartParser.prototype.write = function(buffer) { + var self = this, + i = 0, + len = buffer.length, + prevIndex = this.index, + index = this.index, + state = this.state, + flags = this.flags, + lookbehind = this.lookbehind, + boundary = this.boundary, + boundaryChars = this.boundaryChars, + boundaryLength = this.boundary.length, + boundaryEnd = boundaryLength - 1, + bufferLength = buffer.length, + c, + cl, + + mark = function(name) { + self[name+'Mark'] = i; + }, + clear = function(name) { + delete self[name+'Mark']; + }, + callback = function(name, buffer, start, end) { + if (start !== undefined && start === end) { + return; + } + + var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); + if (callbackSymbol in self) { + self[callbackSymbol](buffer, start, end); + } + }, + dataCallback = function(name, clear) { + var markSymbol = name+'Mark'; + if (!(markSymbol in self)) { + return; + } + + if (!clear) { + callback(name, buffer, self[markSymbol], buffer.length); + self[markSymbol] = 0; + } else { + callback(name, buffer, self[markSymbol], i); + delete self[markSymbol]; + } + }; + + for (i = 0; i < len; i++) { + c = buffer[i]; + switch (state) { + case S.PARSER_UNINITIALIZED: + return i; + case S.START: + index = 0; + state = S.START_BOUNDARY; + case S.START_BOUNDARY: + if (index == boundary.length - 2) { + if (c != CR) { + return i; + } + index++; + break; + } else if (index - 1 == boundary.length - 2) { + if (c != LF) { + return i; + } + index = 0; + 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; + mark('headerField'); + index = 0; + case S.HEADER_FIELD: + if (c == CR) { + clear('headerField'); + state = S.HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c == HYPHEN) { + break; + } + + if (c == COLON) { + if (index == 1) { + // empty header field + return i; + } + dataCallback('headerField', true); + state = S.HEADER_VALUE_START; + break; + } + + cl = lower(c); + if (cl < A || cl > Z) { + return i; + } + break; + case S.HEADER_VALUE_START: + if (c == SPACE) { + break; + } + + mark('headerValue'); + state = S.HEADER_VALUE; + case S.HEADER_VALUE: + if (c == CR) { + dataCallback('headerValue', true); + 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; + } + + callback('headersEnd'); + state = S.PART_DATA_START; + break; + case S.PART_DATA_START: + state = S.PART_DATA + mark('partData'); + 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 < boundary.length) { + if (boundary[index] == c) { + if (index == 0) { + dataCallback('partData', true); + } + index++; + } else { + index = 0; + } + } else if (index == boundary.length) { + 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 == boundary.length) { + if (flags & F.PART_BOUNDARY) { + index = 0; + if (c == LF) { + // unset the PART_BOUNDARY flag + flags &= ~F.PART_BOUNDARY; + callback('partEnd'); + callback('partBegin'); + state = S.HEADER_FIELD_START; + break; + } + } else if (flags & F.LAST_BOUNDARY) { + if (c == HYPHEN) { + callback('partEnd'); + 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 + callback('partData', lookbehind, 0, prevIndex); + prevIndex = 0; + mark('partData'); + + // 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; + } + } + + dataCallback('headerField'); + dataCallback('headerValue'); + dataCallback('partData'); + + this.index = index; + this.state = state; + this.flags = flags; + + return len; +}; + +MultipartParser.prototype.end = function() { + if (this.state != S.END) { + return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); + } +}; + +MultipartParser.prototype.explain = function() { + return 'state = ' + MultipartParser.stateToString(this.state); +}; diff --git a/websvr/lib/querystring_parser.js b/websvr/lib/querystring_parser.js new file mode 100644 index 0000000..b8e86e3 --- /dev/null +++ b/websvr/lib/querystring_parser.js @@ -0,0 +1,25 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +// This is a buffering parser, not quite as nice as the multipart one. +// If I find time I'll rewrite this to be fully streaming as well +var querystring = require('querystring'); + +function QuerystringParser() { + this.buffer = ''; +}; +exports.QuerystringParser = QuerystringParser; + +QuerystringParser.prototype.write = function(buffer) { + this.buffer += buffer.toString('ascii'); + return buffer.length; +}; + +QuerystringParser.prototype.end = function() { + var fields = querystring.parse(this.buffer); + for (var field in fields) { + this.onField(field, fields[field]); + } + this.buffer = ''; + + this.onEnd(); +}; \ No newline at end of file diff --git a/websvr/lib/types/mime.types b/websvr/lib/types/mime.types new file mode 100644 index 0000000..cf9dbe8 --- /dev/null +++ b/websvr/lib/types/mime.types @@ -0,0 +1,1510 @@ +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpp-ims+xml +# application/activemessage +application/andrew-inset ez +# application/applefile +application/applixware aw +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomicmail +application/atomsvc+xml atomsvc +# application/auth-policy+xml +# application/batch-smtp +# application/beep+xml +# application/calendar+xml +# application/cals-1840 +# application/ccmp+xml +application/ccxml+xml ccxml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cnrp+xml +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csta+xml +# application/cstadata+xml +application/cu-seeme cu +# application/cybercash +application/davmount+xml davmount +# application/dca-rft +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dns +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +application/emma+xml emma +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fits +application/font-tdpfr pfr +# application/framework-attributes+xml +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-request+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +application/json json +# application/kpml-request+xml +# application/kpml-response+xml +application/lost+xml lostxml +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +# application/mathml-content+xml +# application/mathml-presentation+xml +application/mathml+xml mathml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media_control+xml +application/mediaservercontrol+xml mscml +application/metalink4+xml meta4 +application/mets+xml mets +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-request +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nss +# application/ocsp-request +# application/ocsp-response +application/octet-stream bin dms lha lrf lzh so iso dmg dist distz pkg bpk dump elc deploy +application/oda oda +application/oebps-package+xml opf +application/ogg ogx +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/tve-trigger +# application/ulpfec +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp.bsf+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +application/vnd.chemdraw+xml cdxml +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.collection+json +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cybank +application/vnd.data-vision.rdz rdz +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.dir-bi.plate-dl-nosuffix +application/vnd.dna dna +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-request+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.fints +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +# application/vnd.fujixerox.hbpl +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +application/vnd.hzn-3d-crossword x3d +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.packageitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-request+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-request +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.quobject-quoxdocument +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +# application/vnd.otps.ct-kip+xml +application/vnd.palm pdb pqa oprc +# application/vnd.paos.xml +application/vnd.pawaafile paw +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.route66.link66+xml link66 +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +# application/vnd.sun.wadl+xml +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.wsc +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cdlink vcd +application/x-chat chat +application/x-chess-pgn pgn +# application/x-compress +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-ttf ttf ttc +application/x-font-type1 pfa pfb pfm afm +application/x-font-woff woff +# application/x-font-vfont +application/x-futuresplash spl +application/x-gnumeric gnumeric +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-stuffit sit +application/x-stuffitx sitx +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-ustar ustar +application/x-wais-source src +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xpinstall xpi +# application/x400-bp +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info-diff+xml +# application/xcon-conference-info+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xmpp+xml +application/xop+xml xop +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/example +# audio/fwdred +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +audio/ogg oga ogg spx +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu-wb +# audio/pcmu +# audio/prs.sid +# audio/qcelp +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtx +# audio/smv +# audio/smv0 +# audio/smv-qcp +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file dvb +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +audio/x-wav wav +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.dvb.subtitle sub +image/vnd.djvu djvu djv +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +image/vnd.ms-modi mdi +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +image/webp webp +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# model/example +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +model/vnd.vtu vtu +model/vrml wrl vrml +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# text/1d-interleaved-parityfec +text/calendar ics ifb +text/css css +text/csv csv +# text/directory +# text/dns +# text/ecmascript +# text/enriched +# text/example +# text/fwdred +text/html html htm +# text/javascript +text/n3 n3 +# text/parityfec +text/plain txt text conf def list log in +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/vnd.radisys.msml-basic-layout +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.scurl scurl +text/vnd.curl.mcurl mcurl +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-pascal p pas +text/x-java-source java +text/x-setext etx +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raw +# video/rtp-enc-aescm128 +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-ms-asf asf asx +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +x-conference/x-cooltalk ice diff --git a/websvr/lib/types/node.types b/websvr/lib/types/node.types new file mode 100644 index 0000000..69a6641 --- /dev/null +++ b/websvr/lib/types/node.types @@ -0,0 +1,54 @@ +# What: Google Chrome Extension +# Why: To allow apps to (work) be served with the right content type header. +# http://codereview.chromium.org/2830017 +# Added by: niftylettuce +application/x-chrome-extension crx + +# What: OTF Message Silencer +# Why: To silence the "Resource interpreted as font but transferred with MIME +# type font/otf" message that occurs in Google Chrome +# Added by: niftylettuce +font/opentype otf + +# What: HTC support +# Why: To properly render .htc files such as CSS3PIE +# Added by: niftylettuce +text/x-component htc + +# What: HTML5 application cache manifest +# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps +# per https://developer.mozilla.org/en/offline_resources_in_firefox +# Added by: louisremi +text/cache-manifest appcache manifest + +# What: node binary buffer format +# Why: semi-standard extension w/in the node community +# Added by: tootallnate +application/octet-stream buffer + +# What: The "protected" MP-4 formats used by iTunes. +# Why: Required for streaming music to browsers (?) +# Added by: broofa +application/mp4 m4p +audio/mp4 m4a + +# What: Music playlist format (http://en.wikipedia.org/wiki/M3U) +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +application/x-mpegURL m3u8 + +# What: Video format, Part of RFC1890 +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +video/MP2T ts + +# What: The FLAC lossless codec format +# Why: Streaming and serving FLAC audio +# Added by: jacobrask +audio/flac flac + +# What: EventSource mime type +# Why: mime type of Server-Sent Events stream +# http://www.w3.org/TR/eventsource/#text-event-stream +# Added by: francois2metz +text/event-stream event-stream diff --git a/websvr/lib/underscore.js b/websvr/lib/underscore.js new file mode 100644 index 0000000..93df9d1 --- /dev/null +++ b/websvr/lib/underscore.js @@ -0,0 +1,1200 @@ +// Underscore.js 1.4.2 +// http://underscorejs.org +// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root['_'] = _; + } + + // Current version. + _.VERSION = '1.4.2'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + each(obj, function(value, index, list) { + if (!iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + found = any(obj, function(value) { + return value === target; + }); + return found; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + return _.map(obj, function(value) { + return (_.isFunction(method) ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // with specific `key:value` pairs. + _.where = function(obj, attrs) { + if (_.isEmpty(attrs)) return []; + return _.filter(obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + index : index, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index < right.index ? -1 : 1; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(obj, value, context, behavior) { + var result = {}; + var iterator = lookupIterator(value); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + }; + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + if (!_.has(result, key)) result[key] = 0; + result[key]++; + }); + }; + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(obj) { + if (!obj) return []; + if (obj.length === +obj.length) return slice.call(obj); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, function(value){ return !!value; }); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + each(input, function(value) { + if (_.isArray(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(concat.apply(ArrayProto, arguments)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(args, "" + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + var result = {}; + for (var i = 0, l = list.length; i < l; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, l = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. + // Delegates to **ECMAScript 5**'s native `Function.bind` if available. + // We check for `func.bind` first, to fail fast when `func` is undefined. + _.bind = function bind(func, context) { + var bound, args; + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length == 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, throttling, more, result; + var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + return function() { + context = this; args = arguments; + var later = function() { + timeout = null; + if (more) { + result = func.apply(context, args); + } + whenDone(); + }; + if (!timeout) timeout = setTimeout(later, wait); + if (throttling) { + more = true; + } else { + throttling = true; + result = func.apply(context, args); + } + whenDone(); + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var values = []; + for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var pairs = []; + for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return _.isNumber(obj) && isFinite(obj); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + for (var i = 0; i < n; i++) iterator.call(context, i); + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + (0 | Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named property is a function then invoke it; + // otherwise, return it. + _.result = function(object, property) { + if (object == null) return null; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = idCounter++; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + source += + escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : + interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : + evaluate ? "';\n" + evaluate + "\n__p+='" : ''; + index = offset + match.length; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + var render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); diff --git a/websvr/lib/util.js b/websvr/lib/util.js new file mode 100644 index 0000000..61f6b4c --- /dev/null +++ b/websvr/lib/util.js @@ -0,0 +1,6 @@ +// Backwards compatibility ... +try { + module.exports = require('util'); +} catch (e) { + module.exports = require('sys'); +} diff --git a/websvr/ref/Math.uuid.js b/websvr/ref/Math.uuid.js new file mode 100644 index 0000000..38a6271 --- /dev/null +++ b/websvr/ref/Math.uuid.js @@ -0,0 +1,92 @@ +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); diff --git a/websvr/tool/Combine.js b/websvr/tool/Combine.js new file mode 100644 index 0000000..fba9156 --- /dev/null +++ b/websvr/tool/Combine.js @@ -0,0 +1,226 @@ +/* +* Description: Combine the files into one, support directory and config files. +* Author: Kris Zhang +* Blog: http://c52u.com +*/ + +//Combine namespace +var Combine; + +(function() { + + var fs = require("fs"), + path = require("path"), + timerID = null; + + var combine = Combine = module.exports = { + sourceFile: "", + targetFile: "", + files: [], //combine list + list: [], //watch list + watch: false, //listen on the changes? + + //Interface + init: function(sourceFile, targetFile, watch) { + //Combine type, it's a directory or cfg file + fs.stat(sourceFile, function(err, stat) { + if (err) { + console.log(err); + return; + } + + combine.sourceFile = sourceFile; + combine.targetFile = targetFile; + combine.watch = watch; + + //It's a configuration files or dictionary + if (stat.isFile()) { + combine.setCfg(sourceFile); + } else { + combine.setDir(sourceFile); + } + }); + }, + + //get output stream + getStream: function() { + var stream; + try { + stream = fs.createWriteStream(combine.targetFile); + } catch (err) { + console.log("Can't create output stream: ", err); + } + return stream; + }, + + //get files from cfgFile, return absolute file path + getFiles: function(cfgPath) { + var contents = fs.readFileSync(cfgPath, 'utf-8'), + files = [], + lastIdx = cfgPath.lastIndexOf('\\'), + dir = cfgPath.substring(0, lastIdx > -1 ? lastIdx : cfgPath.lastIndexOf('/') ); + + //read a file line-by-line + contents.match(/[^\r\n]+/g).forEach(function(line) { + //ignore comments that begin with '#' + if (line[0] != '#') { + files.push(path.join(dir, line)); + } + }); + + combine.watch && files.forEach(function(file) { + if (combine.list.indexOf(file) < 0) { + combine.watchFile(file); + combine.list.push(file); + }; + }); + + combine.files = files; + + return files; + }, + + //Watch changes on source folder + setDir: function(directory) { + //Combine at the first running, then watching the changes. + if (combine.combineDir(directory)) { + combine.watch && fs.watch(directory, function() { + combine.combineDir(directory); + }); + } + }, + + //Watch chagnes on configuration fiel + setCfg: function(configuration) { + var combineCfg = function() { + //get file list from the configuration files. + combine.getFiles(configuration); + combine.combine(); + }; + + //Listen on the change on the configuration file + combine.watch && fs.watch(configuration, combineCfg); + + //combine at the first running + combineCfg(); + }, + + //Watch changes on a file + watchFile: function(file) { + try { + fs.watch(file, function() { + combine.combine(); + }); + } catch (err) { + console.log(file, err); + } + }, + + //Combine directory + combineDir: function(directory) { + try { + var allFiles = fs.readdirSync(directory), + //File name must be consist of numbers characters or "-" "_", "." + fileReg = /^[a-zA-Z0-9-_\.]+$/, + files = []; + + allFiles.forEach(function(file) { + if (fileReg.test(file)) { + files.push(path.join(directory, file)); + } else { + console.log("Skip file:" + file); + } + }); + + combine.files = files; + + return combine.combine(); + } catch (err) { + console.log(err); + return false; + } + }, + + //Combine set of files into one + combine: function() { + + //Prevent other request within 1 seconds; + if (timerID) return; + + timerID = setTimeout(function() { + timerID = null; + }, 1000); + + var oStream = combine.getStream(), r = true; + if (!oStream) { + return false; + } + + var files = combine.files; + + try { + files.forEach(function(file) { + var stat = fs.statSync(file); + + if (!stat.isFile()) { + console.log("Skip folder:" + file); + } else { + var data = fs.readFileSync(file); + oStream.write("/*" + file + "*/\r\n"); + oStream.write(data); + oStream.write("\r\n"); + + console.log("Adding file:" + file); + } + }); + oStream.end(); + + var endTime = new Date(); + console.log("count:", + files.length, + ", date:", new Date().toTimeString(), + "\r\n\r\n" + ); + } catch (err) { + console.log(err); + r = false; + } + + return r; + } + }; + +})(); + + +/* +* call it from command lines +* -i filepath: input directory or cfg file +* -o filepath: output files +* -w: keep watch the changes? +*/ +(function() { + + /* + * parsing parameters from command line + * etc, node combine.js -i configfile.path -o outputfile.path + * the parameter will be: '-' + one character, like: parsing('-o'); + */ + var parsing = function(args, key) { + if (!key || key.length != 2 || key[0] != '-') return; + + var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), + param = args.match(reg); + + if (param && param[0]) { + return param[0].substr(4, 500); + } + }; + + var args = process.argv.join(' '), + input = parsing(args, '-i'), + output = parsing(args, '-o'); + + Combine.init(input, output, args.indexOf(' -w') > 0); + +})(); diff --git a/websvr/websvr.js b/websvr/websvr.js new file mode 100644 index 0000000..d221220 --- /dev/null +++ b/websvr/websvr.js @@ -0,0 +1,838 @@ +/*Global.js*/ +/* +* Description: WebSvr +* Lincense: MIT, GPL +* Author: Kris Zhang +- Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +- Underscore: Add underscore a utility-belt library for JavaScript + https://github.com/documentcloud/underscore +- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime +*/ + +//Underscore global object; +var _ = require("./lib/underscore"); + +//TODO: Should add global reference here? i.e. +/* +var fs = require("fs"); +*/ +/*Settings.js*/ +/* +Configurations +*/ +var Settings = { + version: 0.022, + + //root folder of web + root: "../", + + //list files in directory + listDir: false, + + //http + http: true, + //default port of http + port: 8054, + + //enable debug information output? + debug: false, + + //https + https: false, + //default port of https + httpsPort: 8443, + httpsOpts: { key:"", cert:"" }, + + //session file stored here, must be end with "/" + sessionDir: "../tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "../tmp/upload/" +}; + +/*ref\Math.uuid.js*/ +/*! +Math.uuid.js (v1.4) +http://www.broofa.com +mailto:robert@broofa.com + +Copyright (c) 2010 Robert Kieffer +Dual licensed under the MIT and GPL licenses. +*/ + +/* + * Generate a random uuid. + * + * USAGE: Math.uuid(length, radix) + * length - the desired number of characters + * radix - the number of allowable values for each character. + * + * EXAMPLES: + * // No arguments - returns RFC4122, version 4 ID + * >>> Math.uuid() + * "92329D39-6F5C-4520-ABFC-AAB64544E172" + * + * // One argument - returns ID of the specified length + * >>> Math.uuid(15) // 15 character ID (default base=62) + * "VcydxgltxrVZSTV" + * + * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) + * >>> Math.uuid(8, 2) // 8 character ID (base=2) + * "01001010" + * >>> Math.uuid(8, 10) // 8 character ID (base=10) + * "47473046" + * >>> Math.uuid(8, 16) // 8 character ID (base=16) + * "098F4D35" + */ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); + +/*Mapper.js*/ +/* +Mapper: Used for Filter & Handler, +expression: required parameter +handler: required parameter +options: other optional parameters +*/ + +var Mapper = function(expression, handler, options) { + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); +}; + +Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req) { + var self = this, + expression = self.expression; + + //No expression? It's a general filter mapper + if (!expression) return true; + + switch(expression.constructor) { + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options) { + for(key in options) { + this[key] = options[key] + } + } +}; +/*RequestParser.js*/ +/* +Request parser, parse the data in request body via +when parse complete, execute the callback, with response data; +*/ +var RequestParser; + +(function() { + + //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? + var MAX_SIZE = 16 * 1024 * 1024, + buffer = new Buffer(MAX_SIZE); + + RequestParser = function(req, res, callback) { + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + +}()); +/*SessionParser.js*/ + +var SessionParser; + +//TODO: Need a child process of clear session +(function() { + + var fs = require("fs"); + + SessionParser = (function(req, res, callback) { + + var sessionDir = Settings.sessionDir; + + var self = { + //session id + sid : null, + //session stored object + obj : {} + }; + + //TODO + self.set = function(key, val, callback) { + + var sessionfile = sessionDir + self.sid; + + key && (self.obj[key] = val); + + fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } + + callback && callback(self); + }); + }; + + //TO DO + self.get = function(key) { + return self.obj[key]; + }; + + self.init = function() { + var sidKey = "_wsid", + sidVal, + cookie = req.headers.cookie || ""; + + //Get or Create sid + var idx = cookie.indexOf(sidKey + "="); + + //sid exist in the cookie, read it + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //sid doesn't exist, create it; + if (idx < 0 || sidVal.length != 32) { + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + }; + self.sid = sidVal; + + //We only receive the cookie from Http headers + var sessionfile = sessionDir + self.sid; + + //here will be cause a bit of delay + fs.exists(sessionfile, function (exists) { + if (exists) { + fs.readFile( sessionfile, function (err, data) { + if (err) { + console.log(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + callback(self); + }); + }else{ + //session not exist create one + self.obj = {}; + self.set(null , null , callback); + } + }); + + }; + + self.init(); + + return self; + + }); + +}()); +/*Parser.js*/ +/* +Parser: Functions that Filter and Handler will be called +*/ +var Parser = function(req, res, mapper) { + + var handler = mapper.handler; + + //add sesion support + var parseSession = function() { + //add sesion support + if (mapper.session && typeof req.session == "undefined") { + SessionParser(req, res, function(session) { + req.session = session; + handler(req, res); + }); + }else{ + handler(req, res); + } + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseRequest = function() { + //need to parse the request? + if (mapper.parse && typeof req.body == "undefined") { + //Must parser the request first, or the post data will lost; + RequestParser(req, res, function(data) { + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function() { + //Need to parse the file in request? + if (mapper.file && typeof req.body == "undefined") { + //Must parser the request first, or the post data maybe lost; + var formidable = require('./lib/incoming_form'), + form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; + + form.parse(req, function(err, fields, files) { + if (err) { + console.log(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseRequest(); + }); + }else{ + parseRequest(); + }; + }; + + parseFile(); + +}; + +/*Filter.js*/ +/* +Http Filter: Execute all the rules that matched, +Filters will be always called before a handler. +*/ +var Filter = { + //filter list + filters: [], + + /* + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] + */ + filter: function(expression, handler, options) { + //The first parameter is Function => (handler, options) + if (expression.constructor == Function) { + options = handler; + handler = expression; + expression = null; + } + + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); + }, + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + file: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, {file: true}); + //insert as the first elements + Filter.filters.splice(0, 0, mapper); + } +}; + +/* +Filter Chain +*/ +var FilterChain = function(cb) { + var self = this; + self.idx = 0; + self.cb = cb; +}; + +FilterChain.prototype = { + next: function(req, res) { + var self = this; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if (!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res) { + //filter actions + req.next(req, res); + }, options); + */ + if (mapper.match(req)) { + + console.log("filter matched", self.idx, mapper.expression, req.url); + + Parser(req, res, mapper); + }else{ + self.next(req, res); + } + } +}; +/*Handler.js*/ +/* +Http Handler: Execute and returned when when first matched; +At the same time only one Handler will be called; +*/ +var Handler; + +(function() { + + /* + Private: handler list + */ + var handlers = []; + + /* + Static Handler instance + */ + Handler = { + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ + url: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, options); + handlers.push(mapper); + }, + + //Post: Parse the post data by default; + post: function(expression, handler, options) { + this.url(expression, handler, _.extend({ parse: true }, options)); + }, + + //Session: Parse the session and post by default; + session: function(expression, handler) { + this.url(expression, handler, { parse: true, session: true }); + }, + + handle: function(req, res) { + //flag: is matched? + for(var i = 0, len = handlers.length; i < len ; i++) { + + var mapper = handlers[i]; + if (mapper.match(req)) { + + console.log("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type) { + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + return true; + } + } + + return false; + + } //end of handle + + }; + +}()); + + + + +/*ListDir.js*/ +/* +ListDir: List all the files in a directory +*/ +var ListDir = (function() { + + var fs = require("fs"), + path = require("path"); + + var urlFormat = function(url) { + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date) { + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num) { + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url) { + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list: function(req, res, dir) { + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function() { + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +      };
    +
    +      var listEnd = function() {
    +        res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files) { + if (err) { + listEnd(); + console.log(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++) { + //Persistent the idx before make the sync process + (function(idx) { + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat) { + cur++; + + if (err) { + console.log(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + +}()); +/*Server.js*/ +/* +* Description: Create a Web Server +* Author: Kris Zhang +* Licenses: MIT, GPL +*/ +/* +* Define WebSvr +* Export WebSvr +*/ +var WebSvr = module.exports = (function() { + + var server = function(options) { + //Library + var fs = require("fs"), + path = require("path"), + mime = require("./lib/mime"); + + //Parameters + //Count: How many files? + var self = this, + root, + port; + + var fileHandler = function(req, res) { + + var url = req.url, + hasQuery = url.indexOf("?"); + + //fs.stat can't recognize the file name with querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; + + var fullPath = path.join(root, url); + + fs.stat(fullPath, function(err, stat) { + + //Consider as file not found + if (err) return self.write404(res); + + //Is file? Open this file and send to client. + if (stat.isFile()) { + writeFile(res, fullPath); + } + + //Is Directory? List all the files and folders. + else if (stat.isDirectory()) { + options.listDir + ? ListDir.list(req, res, fullPath) + : self.write403(res); + } + + }); + }; + + var requestHandler = function(req, res) { + //Response may be shutdown when do the filter, in order not to cause exception, + //Rewrite the write/writeHead functionalities of current response object + var endFn = res.end; + res.end = function() { + //Execute old end + endFn.apply(res, arguments); + //Rewirte write/writeHead on response object + res.write = res.writeHead = function() { + console.log("response is already end, response.write ignored!") + }; + }; + + res.writeFile = function(filePath, cb) { + self.writeFile(res, filePath, cb); + }; + + res.redirect = function(url, status) { + res.writeHead(status ? status : 302, { "Location": url }); + res.end(); + }; + + //Define filter object + req.filter = new FilterChain(function() { + //if handler not match, send the request + !Handler.handle(req, res) && fileHandler(req, res); + }); + + //Handle the first filter + req.filter.next(req, res); + }; + + var writeFile = function(res, fullPath) { + fs.readFile(fullPath, function(err, data) { + if (err) { + console.log(err); + return; + } + res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + res.end(data, "binary"); + }); + }; + + //Explose API + //Filter + self.filter = Filter.filter; + self.file = Filter.file; + + //Handler + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; + + //Get a fullpath of a request + self.getFullPath = function(filePath) { + return path.join(root, filePath); + }; + + //Write file, filePath is relative path + self.writeFile = function(res, filePath, cb) { + filePath = path.join(root, filePath); + fs.exists(filePath, function(exist) { + if (exist) { + writeFile(res, filePath); + cb && cb(exist); + }else{ + //If callback function doesn't exist, write 404 page; + cb ? cb(exist) : self.write404(res); + } + }); + }; + + //TODO: Support 301 move permanently + + //TODO: Support 304 client-side cache + + self.write403 = function(res) { + res.writeHead(403, {"Content-Type": "text/html"}); + res.end("Access forbidden!"); + }; + + self.write404 = function(res) { + res.writeHead(404, {"Content-Type": "text/html"}); + res.end("File not found!"); + }; + + //Public: start http server + self.start = function() { + //Update the default value of Settings + options = _.extend(Settings, options); + + root = options.root; + port = parseInt(options.port); + + //Create http server + if (options.http) { + var httpSvr = require("http").createServer(requestHandler); + httpSvr.listen(port); + + console.log("Http server running at" + ,"Root:", root + ,"Port:", port + ); + + self.httpSvr = httpSvr; + } + + //Create https server + if (options.https) { + var httpsOpts = options.httpsOpts, + httpsPort = options.httpsPort; + + var httpsSvr = require("https").createServer(httpsOpts, requestHandler); + httpsSvr.listen(httpsPort); + + console.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); + + self.httpsSvr = httpsSvr; + } + + //diable console.log information + if (!options.debug) { + console.log = function(){}; + } + }; + + //Public: close http server; + self.close = function() { + self.httpSvr && self.httpSvr.close(); + self.httpsSvr && self.httpsSvr.close(); + }; + + }; + + return server; + +})(); From 22c1f21a6c79f712edb5a867ee00a7a56956d12d Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 8 Nov 2012 11:32:33 +0800 Subject: [PATCH 027/195] reorganize formatting --- svr/Filter.js | 74 - svr/Global.js | 18 - svr/Handler.js | 81 - svr/ListDir.js | 97 -- svr/MakeFile.list | 23 - svr/Mapper.js | 49 - svr/Parser.js | 70 - svr/RequestParser.js | 27 - svr/SessionParser.js | 91 - svr/Settings.js | 34 - svr/SiteTest.js | 98 -- svr/WebSvr.all.js | 938 ---------- svr/build.bat | 11 - svr/cert/certificate.pem | 17 - svr/cert/privatekey.pem | 15 - svr/lib/file.js | 73 - svr/lib/incoming_form.js | 390 ----- svr/lib/mime.js | 93 - svr/lib/multipart_parser.js | 312 ---- svr/lib/querystring_parser.js | 25 - svr/lib/types/mime.types | 1510 ----------------- svr/lib/types/node.types | 54 - svr/lib/underscore.js | 1200 ------------- svr/lib/util.js | 6 - svr/ref/Math.uuid.js | 92 - svr/tool/Combine.js | 226 --- svr/websvr.js | 841 --------- .../session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL | 1 + tmp/session/readme.txt | 1 - tmp/upload/readme.txt | 1 - 30 files changed, 1 insertion(+), 6467 deletions(-) delete mode 100644 svr/Filter.js delete mode 100644 svr/Global.js delete mode 100644 svr/Handler.js delete mode 100644 svr/ListDir.js delete mode 100644 svr/MakeFile.list delete mode 100644 svr/Mapper.js delete mode 100644 svr/Parser.js delete mode 100644 svr/RequestParser.js delete mode 100644 svr/SessionParser.js delete mode 100644 svr/Settings.js delete mode 100644 svr/SiteTest.js delete mode 100644 svr/WebSvr.all.js delete mode 100644 svr/build.bat delete mode 100644 svr/cert/certificate.pem delete mode 100644 svr/cert/privatekey.pem delete mode 100644 svr/lib/file.js delete mode 100644 svr/lib/incoming_form.js delete mode 100644 svr/lib/mime.js delete mode 100644 svr/lib/multipart_parser.js delete mode 100644 svr/lib/querystring_parser.js delete mode 100644 svr/lib/types/mime.types delete mode 100644 svr/lib/types/node.types delete mode 100644 svr/lib/underscore.js delete mode 100644 svr/lib/util.js delete mode 100644 svr/ref/Math.uuid.js delete mode 100644 svr/tool/Combine.js delete mode 100644 svr/websvr.js create mode 100644 test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL delete mode 100644 tmp/session/readme.txt delete mode 100644 tmp/upload/readme.txt diff --git a/svr/Filter.js b/svr/Filter.js deleted file mode 100644 index 3497163..0000000 --- a/svr/Filter.js +++ /dev/null @@ -1,74 +0,0 @@ -/* -Http Filter: Execute all the rules that matched, -Filters will be always called before a handler. -*/ -var Filter = { - //filter list - filters: [], - - /* - filter: add a new filter - expression: string/regexp [optional] - handler: function [required] - options: object [optional] - */ - filter: function(expression, handler, options) { - //The first parameter is Function => (handler, options) - if (expression.constructor == Function) { - options = handler; - handler = expression; - expression = null; - } - - var mapper = new Mapper(expression, handler, options); - Filter.filters.push(mapper); - }, - - /* - file receiver: it's a specfic filter, - this filter should be always at the top of the filter list - */ - file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); - //insert as the first elements - Filter.filters.splice(0, 0, mapper); - } -}; - -/* -Filter Chain -*/ -var FilterChain = function(cb) { - var self = this; - self.idx = 0; - self.cb = cb; -}; - -FilterChain.prototype = { - next: function(req, res) { - var self = this; - - var mapper = Filter.filters[self.idx++]; - - //filter is complete, execute callback; - if (!mapper) return self.cb && self.cb(); - - /* - If not Matched go to next filter - If matched need to execute the req.next() in callback handler, - e.g: - webSvr.filter(/expression/, function(req, res) { - //filter actions - req.next(req, res); - }, options); - */ - if (mapper.match(req)) { - - console.log("filter matched", self.idx, mapper.expression, req.url); - - Parser(req, res, mapper); - }else{ - self.next(req, res); - } - } -}; \ No newline at end of file diff --git a/svr/Global.js b/svr/Global.js deleted file mode 100644 index 77499b3..0000000 --- a/svr/Global.js +++ /dev/null @@ -1,18 +0,0 @@ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang -- Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -- Underscore: Add underscore a utility-belt library for JavaScript - https://github.com/documentcloud/underscore -- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime -*/ - -//Underscore global object; -var _ = require("./lib/underscore"); - -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); -*/ \ No newline at end of file diff --git a/svr/Handler.js b/svr/Handler.js deleted file mode 100644 index eda6d19..0000000 --- a/svr/Handler.js +++ /dev/null @@ -1,81 +0,0 @@ -/* -Http Handler: Execute and returned when when first matched; -At the same time only one Handler will be called; -*/ -var Handler; - -(function() { - - /* - Private: handler list - */ - var handlers = []; - - /* - Static Handler instance - */ - Handler = { - /* - url: add a new handler - expression: string/regexp [required] - handler: [many types] [required] - options: object [optional] - */ - url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - handlers.push(mapper); - }, - - //Post: Parse the post data by default; - post: function(expression, handler, options) { - this.url(expression, handler, _.extend({ parse: true }, options)); - }, - - //Session: Parse the session and post by default; - session: function(expression, handler) { - this.url(expression, handler, { parse: true, session: true }); - }, - - handle: function(req, res) { - //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++) { - - var mapper = handlers[i]; - if (mapper.match(req)) { - - console.log("handler matched", i, mapper.expression, req.url); - - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type) { - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - return true; - } - } - - return false; - - } //end of handle - - }; - -}()); - - - diff --git a/svr/ListDir.js b/svr/ListDir.js deleted file mode 100644 index 5d773f0..0000000 --- a/svr/ListDir.js +++ /dev/null @@ -1,97 +0,0 @@ -/* -ListDir: List all the files in a directory -*/ -var ListDir = (function() { - - var fs = require("fs"), - path = require("path"); - - var urlFormat = function(url) { - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date) { - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num) { - return num + " ".substring(0, 12 - String(num).length); - }; - - //Create an anchor - var anchor = function(txt, url) { - url = url ? url : "/"; - return '' + txt + ""; - }; - - var listDir = { - //List all the files in a directory - list: function(req, res, dir) { - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function() { - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function() {
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files) { - if (err) { - listEnd(); - console.log(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++) { - //Persistent the idx before make the sync process - (function(idx) { - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat) { - cur++; - - if (err) { - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - } - }; - - return listDir; - -}()); \ No newline at end of file diff --git a/svr/MakeFile.list b/svr/MakeFile.list deleted file mode 100644 index 0845803..0000000 --- a/svr/MakeFile.list +++ /dev/null @@ -1,23 +0,0 @@ -#Config file -#These modules will be crunched to one. -#TODO: Need to support dictionary in the list; - -#Global object and description -Global.js -Settings.js - -#Reference: JavaScript library -ref/Math.uuid.js - -#WebSvr Modules -Mapper.js -RequestParser.js -SessionParser.js -Parser.js -Filter.js -Handler.js -ListDir.js -WebSvr.js - -#Testing Site -SiteTest.js \ No newline at end of file diff --git a/svr/Mapper.js b/svr/Mapper.js deleted file mode 100644 index 2f31181..0000000 --- a/svr/Mapper.js +++ /dev/null @@ -1,49 +0,0 @@ -/* -Mapper: Used for Filter & Handler, -expression: required parameter -handler: required parameter -options: other optional parameters -*/ - -var Mapper = function(expression, handler, options) { - var self = this; - - self.expression = expression; - self.handler = handler; - - //Has other parameters? - self.extend(options); -}; - -Mapper.prototype = { - /* - Does this mapper matched this request? - */ - match: function(req) { - var self = this, - expression = self.expression; - - //No expression? It's a general filter mapper - if (!expression) return true; - - switch(expression.constructor) { - case String: return req.url.indexOf(expression) > -1; - case RegExp: return expression.test(req.url); - } - - return false; - }, - - /* - Add optional parameters on current mapper - i.e: - session: boolean - file: boolean - parse: boolean - */ - extend: function(options) { - for(key in options) { - this[key] = options[key] - } - } -}; \ No newline at end of file diff --git a/svr/Parser.js b/svr/Parser.js deleted file mode 100644 index 33e8753..0000000 --- a/svr/Parser.js +++ /dev/null @@ -1,70 +0,0 @@ -/* -Parser: Functions that Filter and Handler will be called -*/ -var Parser = function(req, res, mapper) { - - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - SessionParser(req, res, function(session) { - req.session = session; - handler(req, res); - }); - }else{ - handler(req, res); - } - }; - - /* - parse data in request, this should be done before parse session, - because session stored in file - */ - var parseRequest = function() { - //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { - //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data) { - req.body = data; - parseSession(); - }); - }else{ - parseSession(); - } - }; - - /* - parse file in request, this should be at the top of the list - */ - var parseFile = function() { - //Need to parse the file in request? - if (mapper.file && typeof req.body == "undefined") { - //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'), - form = new formidable.IncomingForm(); - - form.uploadDir = Settings.uploadDir; - - form.parse(req, function(err, fields, files) { - if (err) { - console.log(err); - return; - }; - - //attach the parameters and files - req.body = fields; - req.files = files; - - //in fact request will not be parsed again, because body is not undefined - parseRequest(); - }); - }else{ - parseRequest(); - }; - }; - - parseFile(); - -}; diff --git a/svr/RequestParser.js b/svr/RequestParser.js deleted file mode 100644 index 2794277..0000000 --- a/svr/RequestParser.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -Request parser, parse the data in request body via -when parse complete, execute the callback, with response data; -*/ -var RequestParser; - -(function() { - - //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE); - - RequestParser = function(req, res, callback) { - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); - }; - -}()); \ No newline at end of file diff --git a/svr/SessionParser.js b/svr/SessionParser.js deleted file mode 100644 index 4ece4e6..0000000 --- a/svr/SessionParser.js +++ /dev/null @@ -1,91 +0,0 @@ - -var SessionParser; - -//TODO: Need a child process of clear session -(function() { - - var fs = require("fs"); - - SessionParser = (function(req, res, callback) { - - var sessionDir = Settings.sessionDir; - - var self = { - //session id - sid : null, - //session stored object - obj : {} - }; - - //TODO - self.set = function(key, val, callback) { - - var sessionfile = sessionDir + self.sid; - - key && (self.obj[key] = val); - - fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { - if (err) { - console.log(err); - return; - } - - callback && callback(self); - }); - }; - - //TO DO - self.get = function(key) { - return self.obj[key]; - }; - - self.init = function() { - var sidKey = "_wsid", - sidVal, - cookie = req.headers.cookie || ""; - - //Get or Create sid - var idx = cookie.indexOf(sidKey + "="); - - //sid exist in the cookie, read it - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - - //sid doesn't exist, create it; - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - }; - self.sid = sidVal; - - //We only receive the cookie from Http headers - var sessionfile = sessionDir + self.sid; - - //here will be cause a bit of delay - fs.exists(sessionfile, function (exists) { - if (exists) { - fs.readFile( sessionfile, function (err, data) { - if (err) { - console.log(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - callback(self); - }); - }else{ - //session not exist create one - self.obj = {}; - self.set(null , null , callback); - } - }); - - }; - - self.init(); - - return self; - - }); - -}()); \ No newline at end of file diff --git a/svr/Settings.js b/svr/Settings.js deleted file mode 100644 index ce80b68..0000000 --- a/svr/Settings.js +++ /dev/null @@ -1,34 +0,0 @@ -/* -Configurations -*/ -var Settings = { - version: 0.022, - - //root folder of web - root: "../", - - //list files in directory - listDir: false, - - //http - http: true, - //default port of http - port: 8054, - - //enable debug information output? - debug: false, - - //https - https: false, - //default port of https - httpsPort: 8443, - httpsOpts: { - key: require("fs").readFileSync("cert/privatekey.pem"), - cert: require("fs").readFileSync("cert/certificate.pem") - }, - - //session file stored here, must be end with "/" - sessionDir: "../tmp/session/", - //tempary upload file stored here, must be end with "/" - uploadDir: "../tmp/upload/" -}; diff --git a/svr/SiteTest.js b/svr/SiteTest.js deleted file mode 100644 index 2d9abfd..0000000 --- a/svr/SiteTest.js +++ /dev/null @@ -1,98 +0,0 @@ - -//Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; -//Trying at: http://localhost:8054 -var webSvr = new WebSvr({ - root: "../", - listDir: true, - https: true, - httpsPort: 8443, - debug: true -}); - -webSvr.start(); - -/* -General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; - session: init the session and stored in req.session; -*/ -webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); - - //Link to next filter - req.filter.next(req, res); -}, {parse:true, session:true}); - -/* -Session Filter: protect test/* folder => (validation by session); -*/ -webSvr.filter(/test\/[\w\.]+/, function(req, res) { - //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - !req.session.get("username") && res.end("You must login, first!"); - } - - //Link to next filter - req.filter.next(req, res); -}); - - -/* -Handler: login.do => (validate the username & password) - username: admin - password: 12345678 -*/ -webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); - if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session) { - //TODO: Add req.redirect / req.forward functionalities; - res.writeHead(200, {"Content-Type": "text/html"}); - res.writeFile("/test/setting.htm"); - }); - }else{ - res.writeHead(401); - res.end("Wrong username/password"); - } -}); - -/* -Uploader: upload.do => (receive handler) -*/ -webSvr.file("upload.do", function(req, res) { - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); -}); - -/* -Redirect: redirect request, try at: http://localhost:8054/redirect -*/ -webSvr.url("redirect", function(req, res) { - res.redirect("/svr/websvr.all.js"); -}); - - -/* -Simple redirect API: -*/ -//Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine -webSvr.url("combine", ["svr/tool/Combine.js"]); -//Mapping "hello" to a string, trying at http://localhost:8054/hello -webSvr.url("hello", "Hello WebSvr!"); -//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post("post.htm", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //Witch session support: "{session: true}" - res.write("You username is " + req.session.get("username")); - res.write('

    '); - res.end('Received : ' + req.body); -}, {session: true}); \ No newline at end of file diff --git a/svr/WebSvr.all.js b/svr/WebSvr.all.js deleted file mode 100644 index 6d778f2..0000000 --- a/svr/WebSvr.all.js +++ /dev/null @@ -1,938 +0,0 @@ -/*Global.js*/ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang -- Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -- Underscore: Add underscore a utility-belt library for JavaScript - https://github.com/documentcloud/underscore -- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime -*/ - -//Underscore global object; -var _ = require("./lib/underscore"); - -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); -*/ -/*Settings.js*/ -/* -Configurations -*/ -var Settings = { - version: 0.022, - - //root folder of web - root: "../", - - //list files in directory - listDir: false, - - //http - http: true, - //default port of http - port: 8054, - - //enable debug information output? - debug: false, - - //https - https: false, - //default port of https - httpsPort: 8443, - httpsOpts: { - key: require("fs").readFileSync("cert/privatekey.pem"), - cert: require("fs").readFileSync("cert/certificate.pem") - }, - - //session file stored here, must be end with "/" - sessionDir: "../tmp/session/", - //tempary upload file stored here, must be end with "/" - uploadDir: "../tmp/upload/" -}; - -/*ref\Math.uuid.js*/ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); - -/*Mapper.js*/ -/* -Mapper: Used for Filter & Handler, -expression: required parameter -handler: required parameter -options: other optional parameters -*/ - -var Mapper = function(expression, handler, options) { - var self = this; - - self.expression = expression; - self.handler = handler; - - //Has other parameters? - self.extend(options); -}; - -Mapper.prototype = { - /* - Does this mapper matched this request? - */ - match: function(req) { - var self = this, - expression = self.expression; - - //No expression? It's a general filter mapper - if (!expression) return true; - - switch(expression.constructor) { - case String: return req.url.indexOf(expression) > -1; - case RegExp: return expression.test(req.url); - } - - return false; - }, - - /* - Add optional parameters on current mapper - i.e: - session: boolean - file: boolean - parse: boolean - */ - extend: function(options) { - for(key in options) { - this[key] = options[key] - } - } -}; -/*RequestParser.js*/ -/* -Request parser, parse the data in request body via -when parse complete, execute the callback, with response data; -*/ -var RequestParser; - -(function() { - - //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE); - - RequestParser = function(req, res, callback) { - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); - }; - -}()); -/*SessionParser.js*/ - -var SessionParser; - -//TODO: Need a child process of clear session -(function() { - - var fs = require("fs"); - - SessionParser = (function(req, res, callback) { - - var sessionDir = Settings.sessionDir; - - var self = { - //session id - sid : null, - //session stored object - obj : {} - }; - - //TODO - self.set = function(key, val, callback) { - - var sessionfile = sessionDir + self.sid; - - key && (self.obj[key] = val); - - fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { - if (err) { - console.log(err); - return; - } - - callback && callback(self); - }); - }; - - //TO DO - self.get = function(key) { - return self.obj[key]; - }; - - self.init = function() { - var sidKey = "_wsid", - sidVal, - cookie = req.headers.cookie || ""; - - //Get or Create sid - var idx = cookie.indexOf(sidKey + "="); - - //sid exist in the cookie, read it - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - - //sid doesn't exist, create it; - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - }; - self.sid = sidVal; - - //We only receive the cookie from Http headers - var sessionfile = sessionDir + self.sid; - - //here will be cause a bit of delay - fs.exists(sessionfile, function (exists) { - if (exists) { - fs.readFile( sessionfile, function (err, data) { - if (err) { - console.log(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - callback(self); - }); - }else{ - //session not exist create one - self.obj = {}; - self.set(null , null , callback); - } - }); - - }; - - self.init(); - - return self; - - }); - -}()); -/*Parser.js*/ -/* -Parser: Functions that Filter and Handler will be called -*/ -var Parser = function(req, res, mapper) { - - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - SessionParser(req, res, function(session) { - req.session = session; - handler(req, res); - }); - }else{ - handler(req, res); - } - }; - - /* - parse data in request, this should be done before parse session, - because session stored in file - */ - var parseRequest = function() { - //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { - //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data) { - req.body = data; - parseSession(); - }); - }else{ - parseSession(); - } - }; - - /* - parse file in request, this should be at the top of the list - */ - var parseFile = function() { - //Need to parse the file in request? - if (mapper.file && typeof req.body == "undefined") { - //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'), - form = new formidable.IncomingForm(); - - form.uploadDir = Settings.uploadDir; - - form.parse(req, function(err, fields, files) { - if (err) { - console.log(err); - return; - }; - - //attach the parameters and files - req.body = fields; - req.files = files; - - //in fact request will not be parsed again, because body is not undefined - parseRequest(); - }); - }else{ - parseRequest(); - }; - }; - - parseFile(); - -}; - -/*Filter.js*/ -/* -Http Filter: Execute all the rules that matched, -Filters will be always called before a handler. -*/ -var Filter = { - //filter list - filters: [], - - /* - filter: add a new filter - expression: string/regexp [optional] - handler: function [required] - options: object [optional] - */ - filter: function(expression, handler, options) { - //The first parameter is Function => (handler, options) - if (expression.constructor == Function) { - options = handler; - handler = expression; - expression = null; - } - - var mapper = new Mapper(expression, handler, options); - Filter.filters.push(mapper); - }, - - /* - file receiver: it's a specfic filter, - this filter should be always at the top of the filter list - */ - file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); - //insert as the first elements - Filter.filters.splice(0, 0, mapper); - } -}; - -/* -Filter Chain -*/ -var FilterChain = function(cb) { - var self = this; - self.idx = 0; - self.cb = cb; -}; - -FilterChain.prototype = { - next: function(req, res) { - var self = this; - - var mapper = Filter.filters[self.idx++]; - - //filter is complete, execute callback; - if (!mapper) return self.cb && self.cb(); - - /* - If not Matched go to next filter - If matched need to execute the req.next() in callback handler, - e.g: - webSvr.filter(/expression/, function(req, res) { - //filter actions - req.next(req, res); - }, options); - */ - if (mapper.match(req)) { - - console.log("filter matched", self.idx, mapper.expression, req.url); - - Parser(req, res, mapper); - }else{ - self.next(req, res); - } - } -}; -/*Handler.js*/ -/* -Http Handler: Execute and returned when when first matched; -At the same time only one Handler will be called; -*/ -var Handler; - -(function() { - - /* - Private: handler list - */ - var handlers = []; - - /* - Static Handler instance - */ - Handler = { - /* - url: add a new handler - expression: string/regexp [required] - handler: [many types] [required] - options: object [optional] - */ - url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - handlers.push(mapper); - }, - - //Post: Parse the post data by default; - post: function(expression, handler, options) { - this.url(expression, handler, _.extend({ parse: true }, options)); - }, - - //Session: Parse the session and post by default; - session: function(expression, handler) { - this.url(expression, handler, { parse: true, session: true }); - }, - - handle: function(req, res) { - //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++) { - - var mapper = handlers[i]; - if (mapper.match(req)) { - - console.log("handler matched", i, mapper.expression, req.url); - - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type) { - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - return true; - } - } - - return false; - - } //end of handle - - }; - -}()); - - - - -/*ListDir.js*/ -/* -ListDir: List all the files in a directory -*/ -var ListDir = (function() { - - var fs = require("fs"), - path = require("path"); - - var urlFormat = function(url) { - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date) { - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num) { - return num + " ".substring(0, 12 - String(num).length); - }; - - //Create an anchor - var anchor = function(txt, url) { - url = url ? url : "/"; - return '' + txt + ""; - }; - - var listDir = { - //List all the files in a directory - list: function(req, res, dir) { - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function() { - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function() {
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files) { - if (err) { - listEnd(); - console.log(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++) { - //Persistent the idx before make the sync process - (function(idx) { - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat) { - cur++; - - if (err) { - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - } - }; - - return listDir; - -}()); -/*WebSvr.js*/ -/* -* Description: Create a Web Server (http based). -* Author: Kris Zhang -*/ -/* -* WebSvr Namespace -*/ -var WebSvr = (function() { - - var server = function(options) { - //Library - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); - - //Parameters - //Count: How many files? - var self = this, - root, - port; - - var fileHandler = function(req, res) { - - var url = req.url, - hasQuery = url.indexOf("?"); - - //Bug: path.join can't recognize the querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(root, url); - - fs.stat(fullPath, function(err, stat) { - - //Consider as file not found - if (err) return self.write404(res); - - //Is file? Open this file and send to client. - if (stat.isFile()) { - writeFile(res, fullPath); - } - - //Is Directory? List all the files and folders. - else if (stat.isDirectory()) { - options.listDir - ? ListDir.list(req, res, fullPath) - : self.write403(res); - } - - }); - }; - - var requestHandler = function(req, res) { - //Response may be shutdown when do the filter, in order not to cause exception, - //Rewrite the write/writeHead functionalities of current response object - var endFn = res.end; - res.end = function() { - //Execute old end - endFn.apply(res, arguments); - //Rewirte write/writeHead on response object - res.write = res.writeHead = function() { - console.log("response is already end, response.write ignored!") - }; - }; - - res.writeFile = function(filePath, cb) { - self.writeFile(res, filePath, cb); - }; - - res.redirect = function(url, status) { - res.writeHead(status ? status : 302, { "Location": url }); - res.end(); - }; - - //Define filter object - req.filter = new FilterChain(function() { - //if handler not match, send the request - !Handler.handle(req, res) && fileHandler(req, res); - }); - - //Handle the first filter - req.filter.next(req, res); - }; - - var writeFile = function(res, fullPath) { - fs.readFile(fullPath, function(err, data) { - if (err) { - console.log(err); - return; - } - res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); - res.end(data, "binary"); - }); - }; - - //Explose API - //Filter - self.filter = Filter.filter; - self.file = Filter.file; - - //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; - - //Get a fullpath of a request - self.getFullPath = function(filePath) { - return path.join(root, filePath); - }; - - //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb) { - filePath = path.join(root, filePath); - fs.exists(filePath, function(exist) { - if (exist) { - writeFile(res, filePath); - cb && cb(exist); - }else{ - //If callback function doesn't exist, write 404 page; - cb ? cb(exist) : self.write404(res); - } - }); - }; - - //TODO: Support 301 move permanently - - //TODO: Support 304 client-side cache - - self.write403 = function(res) { - res.writeHead(403, {"Content-Type": "text/html"}); - res.end("Access forbidden!"); - }; - - self.write404 = function(res) { - res.writeHead(404, {"Content-Type": "text/html"}); - res.end("File not found!"); - }; - - //Public: start http server - self.start = function() { - //Update the default value of Settings - options = _.extend({}, Settings, options); - - root = options.root; - port = parseInt(options.port); - - //Create http server - if (options.http) { - var httpSvr = require("http").createServer(requestHandler); - httpSvr.listen(port); - - console.log("Http server running at" - ,"Root:", root - ,"Port:", port - ); - - self.httpSvr = httpSvr; - } - - //Create https server - if (options.https) { - var httpsOpts = options.httpsOpts, - httpsPort = options.httpsPort; - - var httpsSvr = require("https").createServer(httpsOpts, requestHandler); - httpsSvr.listen(httpsPort); - - console.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); - - self.httpsSvr = httpsSvr; - } - - //diable console.log information - if (!options.debug) { - console.log = function(){}; - } - }; - - //Public: close http server; - self.close = function() { - self.httpSvr && self.httpSvr.close(); - self.httpsSvr && self.httpsSvr.close(); - }; - - }; - - return server; - -})(); -/*SiteTest.js*/ - -//Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; -//Trying at: http://localhost:8054 -var webSvr = new WebSvr({ - root: "../", - listDir: true, - https: true, - httpsPort: 8443, - debug: true -}); - -webSvr.start(); - -/* -General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; - session: init the session and stored in req.session; -*/ -webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); - - //Link to next filter - req.filter.next(req, res); -}, {parse:true, session:true}); - -/* -Session Filter: protect test/* folder => (validation by session); -*/ -webSvr.filter(/test\/[\w\.]+/, function(req, res) { - //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - !req.session.get("username") && res.end("You must login, first!"); - } - - //Link to next filter - req.filter.next(req, res); -}); - - -/* -Handler: login.do => (validate the username & password) - username: admin - password: 12345678 -*/ -webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); - if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session) { - //TODO: Add req.redirect / req.forward functionalities; - res.writeHead(200, {"Content-Type": "text/html"}); - res.writeFile("/test/setting.htm"); - }); - }else{ - res.writeHead(401); - res.end("Wrong username/password"); - } -}); - -/* -Uploader: upload.do => (receive handler) -*/ -webSvr.file("upload.do", function(req, res) { - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); -}); - -/* -Redirect: redirect request, try at: http://localhost:8054/redirect -*/ -webSvr.url("redirect", function(req, res) { - res.redirect("/svr/websvr.all.js"); -}); - - -/* -Simple redirect API: -*/ -//Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine -webSvr.url("combine", ["svr/tool/Combine.js"]); -//Mapping "hello" to a string, trying at http://localhost:8054/hello -webSvr.url("hello", "Hello WebSvr!"); -//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post("post.htm", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //Witch session support: "{session: true}" - res.write("You username is " + req.session.get("username")); - res.write('

    '); - res.end('Received : ' + req.body); -}, {session: true}); diff --git a/svr/build.bat b/svr/build.bat deleted file mode 100644 index 4edeaef..0000000 --- a/svr/build.bat +++ /dev/null @@ -1,11 +0,0 @@ -REM Combine first -node tool/Combine.js -i makefile.list -o WebSvr.all.js - -REM Then excute it -:start -node WebSvr.all.js - -REM When error occured, restart the server. -REM goto start - -pause; \ No newline at end of file diff --git a/svr/cert/certificate.pem b/svr/cert/certificate.pem deleted file mode 100644 index fa2f72c..0000000 --- a/svr/cert/certificate.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICpTCCAg4CCQCISrlY6+bq+DANBgkqhkiG9w0BAQUFADCBljELMAkGA1UEBhMC -Q04xETAPBgNVBAgMCFNoYW5naGFpMREwDwYDVQQHDAhTaGFuZ2hhaTEQMA4GA1UE -CgwHRW5sb2dpYzEfMB0GA1UECwwWUmVzZWFyY2ggJiBEZXZlbG9wbWVudDESMBAG -A1UEAwwJbG9jYWxob3N0MRowGAYJKoZIhvcNAQkBFgtjMnVAbGl2ZS5jbjAeFw0x -MjEwMjgwNzU1MTZaFw0xMjExMjcwNzU1MTZaMIGWMQswCQYDVQQGEwJDTjERMA8G -A1UECAwIU2hhbmdoYWkxETAPBgNVBAcMCFNoYW5naGFpMRAwDgYDVQQKDAdFbmxv -Z2ljMR8wHQYDVQQLDBZSZXNlYXJjaCAmIERldmVsb3BtZW50MRIwEAYDVQQDDAls -b2NhbGhvc3QxGjAYBgkqhkiG9w0BCQEWC2MydUBsaXZlLmNuMIGfMA0GCSqGSIb3 -DQEBAQUAA4GNADCBiQKBgQCgWDgMTdIEafBqczxi4BpFOL741bf3nttkJxS5eMbj -bQvfDX9LHkNpqZFx3jDWecmjBhUY+B2Q1PmiBnwuRLIBaYsg3jT2nWp0fphzBcJ5 -JsPF3S9GjeWrzYZIuK+YRyCarlmzbjn4omidm/4wAtXuXxQYiouTMe9hyuLnxb6s -mQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAB/dirZrcdB4aGUTlSGWt+LmFuz/Xl6x -iWkfjwvQP7h4oCGoSUjoB9vqy5hIkCKxI+PWhW76S7YpJNXHIdjrC7BRyYVIlAmp -Efm4GUlBAdSYo222my1xTF8SaEx1debDfzq2Ho8xeozpnKae4x62MoA+Fp2kphsq -tLTB8YxbjIQy ------END CERTIFICATE----- diff --git a/svr/cert/privatekey.pem b/svr/cert/privatekey.pem deleted file mode 100644 index 446cf0b..0000000 --- a/svr/cert/privatekey.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQCgWDgMTdIEafBqczxi4BpFOL741bf3nttkJxS5eMbjbQvfDX9L -HkNpqZFx3jDWecmjBhUY+B2Q1PmiBnwuRLIBaYsg3jT2nWp0fphzBcJ5JsPF3S9G -jeWrzYZIuK+YRyCarlmzbjn4omidm/4wAtXuXxQYiouTMe9hyuLnxb6smQIDAQAB -AoGAWDpSlMqZPh6A0EIaPxmqut4PjuIiORlrBL/QUoHXhjpxZsmJem7rjw9j3XDy -FIGs5owpPbUAp7nYpkPFPrxD6U3qVwZk/C7K2Lx2vWYlJ9vf8TTWAvIt6yyNuq5x -ocFosz346viky3K5QkyvmT9y3UuQS65GKj8Hv5S9xSolAhUCQQDSGzabd9c1kgex -EH96Fu/EWIE2G2U3f5/MRalAPlCIrVkBo1YjNoO8QDIb61mW0KdunQrtxrOoKDJp -Opxwpkj7AkEAw15qkHWd2yfU4yw7cFWsWOLojs3pyLdSX+7++vT3gLL8y2otbyKi -mWlrQJDgCnbXywUyRJtMGmLrn9X0y5IUewJBAJxGlYVpy+8SoRn4dXjwGoLmeaUv -F0gCa29a2RrpvqkKlst7HBSw9adN8HeHxGlC5WaG9JwLUZHf5C8U40t+w4UCQClz -keaeneSO2fNtQhs+gjfFxRPviofEpZynJ8B1U0IiN9Ks74Dh91/XZyMm2fI+buCr -dJPr40TB8j5SdgLvNpsCQFaYxoFde72QZHBQKYMsOmKldhcSuQKeZ/oppuhjgYqp -9qrtIyQ2MTchc8Yu27WCU975F0UGWQTsNG0BqVtGLik= ------END RSA PRIVATE KEY----- diff --git a/svr/lib/file.js b/svr/lib/file.js deleted file mode 100644 index 362019c..0000000 --- a/svr/lib/file.js +++ /dev/null @@ -1,73 +0,0 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -var util = require('./util'), - WriteStream = require('fs').WriteStream, - EventEmitter = require('events').EventEmitter, - crypto = require('crypto'); - -function File(properties) { - EventEmitter.call(this); - - this.size = 0; - this.path = null; - this.name = null; - this.type = null; - this.hash = null; - this.lastModifiedDate = null; - - this._writeStream = null; - - for (var key in properties) { - this[key] = properties[key]; - } - - if(typeof this.hash === 'string') { - this.hash = crypto.createHash(properties.hash); - } - - this._backwardsCompatibility(); -} -module.exports = File; -util.inherits(File, EventEmitter); - -// @todo Next release: Show error messages when accessing these -File.prototype._backwardsCompatibility = function() { - var self = this; - this.__defineGetter__('length', function() { - return self.size; - }); - this.__defineGetter__('filename', function() { - return self.name; - }); - this.__defineGetter__('mime', function() { - return self.type; - }); -}; - -File.prototype.open = function() { - this._writeStream = new WriteStream(this.path); -}; - -File.prototype.write = function(buffer, cb) { - var self = this; - this._writeStream.write(buffer, function() { - if(self.hash) { - self.hash.update(buffer); - } - self.lastModifiedDate = new Date(); - self.size += buffer.length; - self.emit('progress', self.size); - cb(); - }); -}; - -File.prototype.end = function(cb) { - var self = this; - this._writeStream.end(function() { - if(self.hash) { - self.hash = self.hash.digest('hex'); - } - self.emit('end'); - cb(); - }); -}; diff --git a/svr/lib/incoming_form.js b/svr/lib/incoming_form.js deleted file mode 100644 index 76eb944..0000000 --- a/svr/lib/incoming_form.js +++ /dev/null @@ -1,390 +0,0 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -var fs = require('fs'); -var util = require('./util'), - path = require('path'), - File = require('./file'), - MultipartParser = require('./multipart_parser').MultipartParser, - QuerystringParser = require('./querystring_parser').QuerystringParser, - StringDecoder = require('string_decoder').StringDecoder, - EventEmitter = require('events').EventEmitter, - Stream = require('stream').Stream; - -function IncomingForm(opts) { - if (!(this instanceof IncomingForm)) return new IncomingForm; - EventEmitter.call(this); - - opts=opts||{}; - - this.error = null; - this.ended = false; - - this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; - this.keepExtensions = opts.keepExtensions || false; - this.uploadDir = opts.uploadDir || IncomingForm.UPLOAD_DIR; - this.encoding = opts.encoding || 'utf-8'; - this.headers = null; - this.type = null; - this.hash = false; - - this.bytesReceived = null; - this.bytesExpected = null; - - this._parser = null; - this._flushing = 0; - this._fieldsSize = 0; -}; -util.inherits(IncomingForm, EventEmitter); -exports.IncomingForm = IncomingForm; - -IncomingForm.UPLOAD_DIR = (function() { - var dirs = [ - process.env.TMP, - process.env.TMPDIR, - process.env.TEMP, - '/tmp', - process.cwd() - ]; - for (var i = 0; i < dirs.length; i++) { - var dir = dirs[i]; - var isDirectory = false; - - try { - isDirectory = fs.statSync(dir).isDirectory(); - } catch (e) {} - - if (isDirectory) return dir; - } -})(); - -IncomingForm.prototype.parse = function(req, cb) { - this.pause = function() { - try { - req.pause(); - } catch (err) { - // the stream was destroyed - if (!this.ended) { - // before it was completed, crash & burn - this._error(err); - } - return false; - } - return true; - }; - - this.resume = function() { - try { - req.resume(); - } catch (err) { - // the stream was destroyed - if (!this.ended) { - // before it was completed, crash & burn - this._error(err); - } - return false; - } - - return true; - }; - - this.writeHeaders(req.headers); - - var self = this; - req - .on('error', function(err) { - self._error(err); - }) - .on('aborted', function() { - self.emit('aborted'); - }) - .on('data', function(buffer) { - self.write(buffer); - }) - .on('end', function() { - if (self.error) { - return; - } - - var err = self._parser.end(); - if (err) { - self._error(err); - } - }); - - if (cb) { - var fields = {}, files = {}; - this - .on('field', function(name, value) { - fields[name] = value; - }) - .on('file', function(name, file) { - files[name] = file; - }) - .on('error', function(err) { - cb(err, fields, files); - }) - .on('end', function() { - cb(null, fields, files); - }); - } - - return this; -}; - -IncomingForm.prototype.writeHeaders = function(headers) { - this.headers = headers; - this._parseContentLength(); - this._parseContentType(); -}; - -IncomingForm.prototype.write = function(buffer) { - if (!this._parser) { - this._error(new Error('unintialized parser')); - return; - } - - this.bytesReceived += buffer.length; - this.emit('progress', this.bytesReceived, this.bytesExpected); - - var bytesParsed = this._parser.write(buffer); - if (bytesParsed !== buffer.length) { - this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); - } - - return bytesParsed; -}; - -IncomingForm.prototype.pause = function() { - // this does nothing, unless overwritten in IncomingForm.parse - return false; -}; - -IncomingForm.prototype.resume = function() { - // this does nothing, unless overwritten in IncomingForm.parse - return false; -}; - -IncomingForm.prototype.onPart = function(part) { - // this method can be overwritten by the user - this.handlePart(part); -}; - -IncomingForm.prototype.handlePart = function(part) { - var self = this; - - if (part.filename === undefined) { - var value = '' - , decoder = new StringDecoder(this.encoding); - - part.on('data', function(buffer) { - self._fieldsSize += buffer.length; - if (self._fieldsSize > self.maxFieldsSize) { - self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); - return; - } - value += decoder.write(buffer); - }); - - part.on('end', function() { - self.emit('field', part.name, value); - }); - return; - } - - this._flushing++; - - var file = new File({ - path: this._uploadPath(part.filename), - name: part.filename, - type: part.mime, - hash: self.hash - }); - - this.emit('fileBegin', part.name, file); - - file.open(); - - part.on('data', function(buffer) { - self.pause(); - file.write(buffer, function() { - self.resume(); - }); - }); - - part.on('end', function() { - file.end(function() { - self._flushing--; - self.emit('file', part.name, file); - self._maybeEnd(); - }); - }); -}; - -IncomingForm.prototype._parseContentType = function() { - if (!this.headers['content-type']) { - this._error(new Error('bad content-type header, no content-type')); - return; - } - - if (this.headers['content-type'].match(/urlencoded/i)) { - this._initUrlencoded(); - return; - } - - if (this.headers['content-type'].match(/multipart/i)) { - var m; - if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { - this._initMultipart(m[1] || m[2]); - } else { - this._error(new Error('bad content-type header, no multipart boundary')); - } - return; - } - - this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); -}; - -IncomingForm.prototype._error = function(err) { - if (this.error) { - return; - } - - this.error = err; - this.pause(); - this.emit('error', err); -}; - -IncomingForm.prototype._parseContentLength = function() { - if (this.headers['content-length']) { - this.bytesReceived = 0; - this.bytesExpected = parseInt(this.headers['content-length'], 10); - this.emit('progress', this.bytesReceived, this.bytesExpected); - } -}; - -IncomingForm.prototype._newParser = function() { - return new MultipartParser(); -}; - -IncomingForm.prototype._initMultipart = function(boundary) { - this.type = 'multipart'; - - var parser = new MultipartParser(), - self = this, - headerField, - headerValue, - part; - - parser.initWithBoundary(boundary); - - parser.onPartBegin = function() { - part = new Stream(); - part.readable = true; - part.headers = {}; - part.name = null; - part.filename = null; - part.mime = null; - headerField = ''; - headerValue = ''; - }; - - parser.onHeaderField = function(b, start, end) { - headerField += b.toString(self.encoding, start, end); - }; - - parser.onHeaderValue = function(b, start, end) { - headerValue += b.toString(self.encoding, start, end); - }; - - parser.onHeaderEnd = function() { - headerField = headerField.toLowerCase(); - part.headers[headerField] = headerValue; - - var m; - if (headerField == 'content-disposition') { - if (m = headerValue.match(/name="([^"]+)"/i)) { - part.name = m[1]; - } - - part.filename = self._fileName(headerValue); - } else if (headerField == 'content-type') { - part.mime = headerValue; - } - - headerField = ''; - headerValue = ''; - }; - - parser.onHeadersEnd = function() { - self.onPart(part); - }; - - parser.onPartData = function(b, start, end) { - part.emit('data', b.slice(start, end)); - }; - - parser.onPartEnd = function() { - part.emit('end'); - }; - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._fileName = function(headerValue) { - var m = headerValue.match(/filename="(.*?)"($|; )/i) - if (!m) return; - - var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); - filename = filename.replace(/%22/g, '"'); - filename = filename.replace(/&#([\d]{4});/g, function(m, code) { - return String.fromCharCode(code); - }); - return filename; -}; - -IncomingForm.prototype._initUrlencoded = function() { - this.type = 'urlencoded'; - - var parser = new QuerystringParser() - , self = this; - - parser.onField = function(key, val) { - self.emit('field', key, val); - }; - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._uploadPath = function(filename) { - var name = ''; - for (var i = 0; i < 32; i++) { - name += Math.floor(Math.random() * 16).toString(16); - } - - if (this.keepExtensions) { - var ext = path.extname(filename); - ext = ext.replace(/(\.[a-z0-9]+).*/, '$1') - - name += ext; - } - - return path.join(this.uploadDir, name); -}; - -IncomingForm.prototype._maybeEnd = function() { - if (!this.ended || this._flushing) { - return; - } - - this.emit('end'); -}; diff --git a/svr/lib/mime.js b/svr/lib/mime.js deleted file mode 100644 index 28dcbfe..0000000 --- a/svr/lib/mime.js +++ /dev/null @@ -1,93 +0,0 @@ -var path = require('path'), - fs = require('fs'); - -var mime = module.exports = { - // Map of extension to mime type - types: Object.create(null), - - // Map of mime type to extension - extensions :Object.create(null), - - /** - * Define mimetype -> extension mappings. Each key is a mime-type that maps - * to an array of extensions associated with the type. The first extension is - * used as the default extension for the type. - * - * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); - * - * @param map (Object) type definitions - */ - define: function(map) { - for (var type in map) { - var exts = map[type]; - - for (var i = 0; i < exts.length; i++) { - mime.types[exts[i]] = type; - } - - // Default extension is the first one we encounter - if (!mime.extensions[type]) { - mime.extensions[type] = exts[0]; - } - } - }, - - /** - * Load an Apache2-style ".types" file - * - * This may be called multiple times (it's expected). Where files declare - * overlapping types/extensions, the last file wins. - * - * @param file (String) path of file to load. - */ - load: function(file) { - // Read file and split into lines - var map = {}, - content = fs.readFileSync(file, 'ascii'), - lines = content.split(/[\r\n]+/); - - lines.forEach(function(line, lineno) { - // Clean up whitespace/comments, and split into fields - var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); - map[fields.shift()] = fields; - }); - - mime.define(map); - }, - - /** - * Lookup a mime type based on extension - */ - lookup: function(path, fallback) { - var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); - - return mime.types[ext] || fallback || mime.default_type - }, - - /** - * Return file extension associated with a mime type - */ - extension: function(mimeType) { - return mime.extensions[mimeType]; - }, - - /** - * Lookup a charset based on mime type. - */ - charsets: { - lookup: function (mimeType, fallback) { - // Assume text types are utf8. Modify mime logic as needed. - return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; - } - } -}; - -// Load our local copy of -// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -mime.load(path.join(__dirname, 'types/mime.types')); - -// Overlay enhancements submitted by the node.js community -mime.load(path.join(__dirname, 'types/node.types')); - -// Set the default type -mime.default_type = mime.types.bin; diff --git a/svr/lib/multipart_parser.js b/svr/lib/multipart_parser.js deleted file mode 100644 index f73140c..0000000 --- a/svr/lib/multipart_parser.js +++ /dev/null @@ -1,312 +0,0 @@ -var Buffer = require('buffer').Buffer, - s = 0, - S = - { PARSER_UNINITIALIZED: 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, - Z = 122, - - lower = function(c) { - return c | 0x20; - }; - -for (var s in S) { - exports[s] = S[s]; -} - -function MultipartParser() { - this.boundary = null; - this.boundaryChars = null; - this.lookbehind = null; - this.state = S.PARSER_UNINITIALIZED; - - this.index = null; - this.flags = 0; -}; -exports.MultipartParser = MultipartParser; - -MultipartParser.stateToString = function(stateNumber) { - for (var state in S) { - var number = S[state]; - if (number === stateNumber) return state; - } -}; - -MultipartParser.prototype.initWithBoundary = function(str) { - this.boundary = new Buffer(str.length+4); - this.boundary.write('\r\n--', 'ascii', 0); - this.boundary.write(str, 'ascii', 4); - this.lookbehind = new Buffer(this.boundary.length+8); - this.state = S.START; - - this.boundaryChars = {}; - for (var i = 0; i < this.boundary.length; i++) { - this.boundaryChars[this.boundary[i]] = true; - } -}; - -MultipartParser.prototype.write = function(buffer) { - var self = this, - i = 0, - len = buffer.length, - prevIndex = this.index, - index = this.index, - state = this.state, - flags = this.flags, - lookbehind = this.lookbehind, - boundary = this.boundary, - boundaryChars = this.boundaryChars, - boundaryLength = this.boundary.length, - boundaryEnd = boundaryLength - 1, - bufferLength = buffer.length, - c, - cl, - - mark = function(name) { - self[name+'Mark'] = i; - }, - clear = function(name) { - delete self[name+'Mark']; - }, - callback = function(name, buffer, start, end) { - if (start !== undefined && start === end) { - return; - } - - var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); - if (callbackSymbol in self) { - self[callbackSymbol](buffer, start, end); - } - }, - dataCallback = function(name, clear) { - var markSymbol = name+'Mark'; - if (!(markSymbol in self)) { - return; - } - - if (!clear) { - callback(name, buffer, self[markSymbol], buffer.length); - self[markSymbol] = 0; - } else { - callback(name, buffer, self[markSymbol], i); - delete self[markSymbol]; - } - }; - - for (i = 0; i < len; i++) { - c = buffer[i]; - switch (state) { - case S.PARSER_UNINITIALIZED: - return i; - case S.START: - index = 0; - state = S.START_BOUNDARY; - case S.START_BOUNDARY: - if (index == boundary.length - 2) { - if (c != CR) { - return i; - } - index++; - break; - } else if (index - 1 == boundary.length - 2) { - if (c != LF) { - return i; - } - index = 0; - 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; - mark('headerField'); - index = 0; - case S.HEADER_FIELD: - if (c == CR) { - clear('headerField'); - state = S.HEADERS_ALMOST_DONE; - break; - } - - index++; - if (c == HYPHEN) { - break; - } - - if (c == COLON) { - if (index == 1) { - // empty header field - return i; - } - dataCallback('headerField', true); - state = S.HEADER_VALUE_START; - break; - } - - cl = lower(c); - if (cl < A || cl > Z) { - return i; - } - break; - case S.HEADER_VALUE_START: - if (c == SPACE) { - break; - } - - mark('headerValue'); - state = S.HEADER_VALUE; - case S.HEADER_VALUE: - if (c == CR) { - dataCallback('headerValue', true); - 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; - } - - callback('headersEnd'); - state = S.PART_DATA_START; - break; - case S.PART_DATA_START: - state = S.PART_DATA - mark('partData'); - 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 < boundary.length) { - if (boundary[index] == c) { - if (index == 0) { - dataCallback('partData', true); - } - index++; - } else { - index = 0; - } - } else if (index == boundary.length) { - 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 == boundary.length) { - if (flags & F.PART_BOUNDARY) { - index = 0; - if (c == LF) { - // unset the PART_BOUNDARY flag - flags &= ~F.PART_BOUNDARY; - callback('partEnd'); - callback('partBegin'); - state = S.HEADER_FIELD_START; - break; - } - } else if (flags & F.LAST_BOUNDARY) { - if (c == HYPHEN) { - callback('partEnd'); - 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 - callback('partData', lookbehind, 0, prevIndex); - prevIndex = 0; - mark('partData'); - - // 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; - } - } - - dataCallback('headerField'); - dataCallback('headerValue'); - dataCallback('partData'); - - this.index = index; - this.state = state; - this.flags = flags; - - return len; -}; - -MultipartParser.prototype.end = function() { - if (this.state != S.END) { - return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); - } -}; - -MultipartParser.prototype.explain = function() { - return 'state = ' + MultipartParser.stateToString(this.state); -}; diff --git a/svr/lib/querystring_parser.js b/svr/lib/querystring_parser.js deleted file mode 100644 index b8e86e3..0000000 --- a/svr/lib/querystring_parser.js +++ /dev/null @@ -1,25 +0,0 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -// This is a buffering parser, not quite as nice as the multipart one. -// If I find time I'll rewrite this to be fully streaming as well -var querystring = require('querystring'); - -function QuerystringParser() { - this.buffer = ''; -}; -exports.QuerystringParser = QuerystringParser; - -QuerystringParser.prototype.write = function(buffer) { - this.buffer += buffer.toString('ascii'); - return buffer.length; -}; - -QuerystringParser.prototype.end = function() { - var fields = querystring.parse(this.buffer); - for (var field in fields) { - this.onField(field, fields[field]); - } - this.buffer = ''; - - this.onEnd(); -}; \ No newline at end of file diff --git a/svr/lib/types/mime.types b/svr/lib/types/mime.types deleted file mode 100644 index cf9dbe8..0000000 --- a/svr/lib/types/mime.types +++ /dev/null @@ -1,1510 +0,0 @@ -# This file maps Internet media types to unique file extension(s). -# Although created for httpd, this file is used by many software systems -# and has been placed in the public domain for unlimited redisribution. -# -# The table below contains both registered and (common) unregistered types. -# A type that has no unique extension can be ignored -- they are listed -# here to guide configurations toward known types and to make it easier to -# identify "new" types. File extensions are also commonly used to indicate -# content languages and encodings, so choose them carefully. -# -# Internet media types should be registered as described in RFC 4288. -# The registry is at . -# -# MIME type (lowercased) Extensions -# ============================================ ========== -# application/1d-interleaved-parityfec -# application/3gpp-ims+xml -# application/activemessage -application/andrew-inset ez -# application/applefile -application/applixware aw -application/atom+xml atom -application/atomcat+xml atomcat -# application/atomicmail -application/atomsvc+xml atomsvc -# application/auth-policy+xml -# application/batch-smtp -# application/beep+xml -# application/calendar+xml -# application/cals-1840 -# application/ccmp+xml -application/ccxml+xml ccxml -application/cdmi-capability cdmia -application/cdmi-container cdmic -application/cdmi-domain cdmid -application/cdmi-object cdmio -application/cdmi-queue cdmiq -# application/cea-2018+xml -# application/cellml+xml -# application/cfw -# application/cnrp+xml -# application/commonground -# application/conference-info+xml -# application/cpl+xml -# application/csta+xml -# application/cstadata+xml -application/cu-seeme cu -# application/cybercash -application/davmount+xml davmount -# application/dca-rft -# application/dec-dx -# application/dialog-info+xml -# application/dicom -# application/dns -# application/dskpp+xml -application/dssc+der dssc -application/dssc+xml xdssc -# application/dvcs -application/ecmascript ecma -# application/edi-consent -# application/edi-x12 -# application/edifact -application/emma+xml emma -# application/epp+xml -application/epub+zip epub -# application/eshop -# application/example -application/exi exi -# application/fastinfoset -# application/fastsoap -# application/fits -application/font-tdpfr pfr -# application/framework-attributes+xml -# application/h224 -# application/held+xml -# application/http -application/hyperstudio stk -# application/ibe-key-request+xml -# application/ibe-pkg-reply+xml -# application/ibe-pp-data -# application/iges -# application/im-iscomposing+xml -# application/index -# application/index.cmd -# application/index.obj -# application/index.response -# application/index.vnd -application/inkml+xml ink inkml -# application/iotp -application/ipfix ipfix -# application/ipp -# application/isup -application/java-archive jar -application/java-serialized-object ser -application/java-vm class -application/javascript js -application/json json -# application/kpml-request+xml -# application/kpml-response+xml -application/lost+xml lostxml -application/mac-binhex40 hqx -application/mac-compactpro cpt -# application/macwriteii -application/mads+xml mads -application/marc mrc -application/marcxml+xml mrcx -application/mathematica ma nb mb -# application/mathml-content+xml -# application/mathml-presentation+xml -application/mathml+xml mathml -# application/mbms-associated-procedure-description+xml -# application/mbms-deregister+xml -# application/mbms-envelope+xml -# application/mbms-msk+xml -# application/mbms-msk-response+xml -# application/mbms-protection-description+xml -# application/mbms-reception-report+xml -# application/mbms-register+xml -# application/mbms-register-response+xml -# application/mbms-user-service-description+xml -application/mbox mbox -# application/media_control+xml -application/mediaservercontrol+xml mscml -application/metalink4+xml meta4 -application/mets+xml mets -# application/mikey -application/mods+xml mods -# application/moss-keys -# application/moss-signature -# application/mosskey-data -# application/mosskey-request -application/mp21 m21 mp21 -application/mp4 mp4s -# application/mpeg4-generic -# application/mpeg4-iod -# application/mpeg4-iod-xmt -# application/msc-ivr+xml -# application/msc-mixer+xml -application/msword doc dot -application/mxf mxf -# application/nasdata -# application/news-checkgroups -# application/news-groupinfo -# application/news-transmission -# application/nss -# application/ocsp-request -# application/ocsp-response -application/octet-stream bin dms lha lrf lzh so iso dmg dist distz pkg bpk dump elc deploy -application/oda oda -application/oebps-package+xml opf -application/ogg ogx -application/onenote onetoc onetoc2 onetmp onepkg -application/oxps oxps -# application/parityfec -application/patch-ops-error+xml xer -application/pdf pdf -application/pgp-encrypted pgp -# application/pgp-keys -application/pgp-signature asc sig -application/pics-rules prf -# application/pidf+xml -# application/pidf-diff+xml -application/pkcs10 p10 -application/pkcs7-mime p7m p7c -application/pkcs7-signature p7s -application/pkcs8 p8 -application/pkix-attr-cert ac -application/pkix-cert cer -application/pkix-crl crl -application/pkix-pkipath pkipath -application/pkixcmp pki -application/pls+xml pls -# application/poc-settings+xml -application/postscript ai eps ps -# application/prs.alvestrand.titrax-sheet -application/prs.cww cww -# application/prs.nprend -# application/prs.plucker -# application/prs.rdf-xml-crypt -# application/prs.xsf+xml -application/pskc+xml pskcxml -# application/qsig -application/rdf+xml rdf -application/reginfo+xml rif -application/relax-ng-compact-syntax rnc -# application/remote-printing -application/resource-lists+xml rl -application/resource-lists-diff+xml rld -# application/riscos -# application/rlmi+xml -application/rls-services+xml rs -application/rpki-ghostbusters gbr -application/rpki-manifest mft -application/rpki-roa roa -# application/rpki-updown -application/rsd+xml rsd -application/rss+xml rss -application/rtf rtf -# application/rtx -# application/samlassertion+xml -# application/samlmetadata+xml -application/sbml+xml sbml -application/scvp-cv-request scq -application/scvp-cv-response scs -application/scvp-vp-request spq -application/scvp-vp-response spp -application/sdp sdp -# application/set-payment -application/set-payment-initiation setpay -# application/set-registration -application/set-registration-initiation setreg -# application/sgml -# application/sgml-open-catalog -application/shf+xml shf -# application/sieve -# application/simple-filter+xml -# application/simple-message-summary -# application/simplesymbolcontainer -# application/slate -# application/smil -application/smil+xml smi smil -# application/soap+fastinfoset -# application/soap+xml -application/sparql-query rq -application/sparql-results+xml srx -# application/spirits-event+xml -application/srgs gram -application/srgs+xml grxml -application/sru+xml sru -application/ssml+xml ssml -# application/tamp-apex-update -# application/tamp-apex-update-confirm -# application/tamp-community-update -# application/tamp-community-update-confirm -# application/tamp-error -# application/tamp-sequence-adjust -# application/tamp-sequence-adjust-confirm -# application/tamp-status-query -# application/tamp-status-response -# application/tamp-update -# application/tamp-update-confirm -application/tei+xml tei teicorpus -application/thraud+xml tfi -# application/timestamp-query -# application/timestamp-reply -application/timestamped-data tsd -# application/tve-trigger -# application/ulpfec -# application/vcard+xml -# application/vemmi -# application/vividence.scriptfile -# application/vnd.3gpp.bsf+xml -application/vnd.3gpp.pic-bw-large plb -application/vnd.3gpp.pic-bw-small psb -application/vnd.3gpp.pic-bw-var pvb -# application/vnd.3gpp.sms -# application/vnd.3gpp2.bcmcsinfo+xml -# application/vnd.3gpp2.sms -application/vnd.3gpp2.tcap tcap -application/vnd.3m.post-it-notes pwn -application/vnd.accpac.simply.aso aso -application/vnd.accpac.simply.imp imp -application/vnd.acucobol acu -application/vnd.acucorp atc acutc -application/vnd.adobe.air-application-installer-package+zip air -application/vnd.adobe.fxp fxp fxpl -# application/vnd.adobe.partial-upload -application/vnd.adobe.xdp+xml xdp -application/vnd.adobe.xfdf xfdf -# application/vnd.aether.imp -# application/vnd.ah-barcode -application/vnd.ahead.space ahead -application/vnd.airzip.filesecure.azf azf -application/vnd.airzip.filesecure.azs azs -application/vnd.amazon.ebook azw -application/vnd.americandynamics.acc acc -application/vnd.amiga.ami ami -# application/vnd.amundsen.maze+xml -application/vnd.android.package-archive apk -application/vnd.anser-web-certificate-issue-initiation cii -application/vnd.anser-web-funds-transfer-initiation fti -application/vnd.antix.game-component atx -application/vnd.apple.installer+xml mpkg -application/vnd.apple.mpegurl m3u8 -# application/vnd.arastra.swi -application/vnd.aristanetworks.swi swi -application/vnd.astraea-software.iota iota -application/vnd.audiograph aep -# application/vnd.autopackage -# application/vnd.avistar+xml -application/vnd.blueice.multipass mpm -# application/vnd.bluetooth.ep.oob -application/vnd.bmi bmi -application/vnd.businessobjects rep -# application/vnd.cab-jscript -# application/vnd.canon-cpdl -# application/vnd.canon-lips -# application/vnd.cendio.thinlinc.clientconf -application/vnd.chemdraw+xml cdxml -application/vnd.chipnuts.karaoke-mmd mmd -application/vnd.cinderella cdy -# application/vnd.cirpack.isdn-ext -application/vnd.claymore cla -application/vnd.cloanto.rp9 rp9 -application/vnd.clonk.c4group c4g c4d c4f c4p c4u -application/vnd.cluetrust.cartomobile-config c11amc -application/vnd.cluetrust.cartomobile-config-pkg c11amz -# application/vnd.collection+json -# application/vnd.commerce-battelle -application/vnd.commonspace csp -application/vnd.contact.cmsg cdbcmsg -application/vnd.cosmocaller cmc -application/vnd.crick.clicker clkx -application/vnd.crick.clicker.keyboard clkk -application/vnd.crick.clicker.palette clkp -application/vnd.crick.clicker.template clkt -application/vnd.crick.clicker.wordbank clkw -application/vnd.criticaltools.wbs+xml wbs -application/vnd.ctc-posml pml -# application/vnd.ctct.ws+xml -# application/vnd.cups-pdf -# application/vnd.cups-postscript -application/vnd.cups-ppd ppd -# application/vnd.cups-raster -# application/vnd.cups-raw -# application/vnd.curl -application/vnd.curl.car car -application/vnd.curl.pcurl pcurl -# application/vnd.cybank -application/vnd.data-vision.rdz rdz -application/vnd.dece.data uvf uvvf uvd uvvd -application/vnd.dece.ttml+xml uvt uvvt -application/vnd.dece.unspecified uvx uvvx -application/vnd.dece.zip uvz uvvz -application/vnd.denovo.fcselayout-link fe_launch -# application/vnd.dir-bi.plate-dl-nosuffix -application/vnd.dna dna -application/vnd.dolby.mlp mlp -# application/vnd.dolby.mobile.1 -# application/vnd.dolby.mobile.2 -application/vnd.dpgraph dpg -application/vnd.dreamfactory dfac -application/vnd.dvb.ait ait -# application/vnd.dvb.dvbj -# application/vnd.dvb.esgcontainer -# application/vnd.dvb.ipdcdftnotifaccess -# application/vnd.dvb.ipdcesgaccess -# application/vnd.dvb.ipdcesgaccess2 -# application/vnd.dvb.ipdcesgpdd -# application/vnd.dvb.ipdcroaming -# application/vnd.dvb.iptv.alfec-base -# application/vnd.dvb.iptv.alfec-enhancement -# application/vnd.dvb.notif-aggregate-root+xml -# application/vnd.dvb.notif-container+xml -# application/vnd.dvb.notif-generic+xml -# application/vnd.dvb.notif-ia-msglist+xml -# application/vnd.dvb.notif-ia-registration-request+xml -# application/vnd.dvb.notif-ia-registration-response+xml -# application/vnd.dvb.notif-init+xml -# application/vnd.dvb.pfr -application/vnd.dvb.service svc -# application/vnd.dxr -application/vnd.dynageo geo -# application/vnd.easykaraoke.cdgdownload -# application/vnd.ecdis-update -application/vnd.ecowin.chart mag -# application/vnd.ecowin.filerequest -# application/vnd.ecowin.fileupdate -# application/vnd.ecowin.series -# application/vnd.ecowin.seriesrequest -# application/vnd.ecowin.seriesupdate -# application/vnd.emclient.accessrequest+xml -application/vnd.enliven nml -# application/vnd.eprints.data+xml -application/vnd.epson.esf esf -application/vnd.epson.msf msf -application/vnd.epson.quickanime qam -application/vnd.epson.salt slt -application/vnd.epson.ssf ssf -# application/vnd.ericsson.quickcall -application/vnd.eszigno3+xml es3 et3 -# application/vnd.etsi.aoc+xml -# application/vnd.etsi.cug+xml -# application/vnd.etsi.iptvcommand+xml -# application/vnd.etsi.iptvdiscovery+xml -# application/vnd.etsi.iptvprofile+xml -# application/vnd.etsi.iptvsad-bc+xml -# application/vnd.etsi.iptvsad-cod+xml -# application/vnd.etsi.iptvsad-npvr+xml -# application/vnd.etsi.iptvservice+xml -# application/vnd.etsi.iptvsync+xml -# application/vnd.etsi.iptvueprofile+xml -# application/vnd.etsi.mcid+xml -# application/vnd.etsi.overload-control-policy-dataset+xml -# application/vnd.etsi.sci+xml -# application/vnd.etsi.simservs+xml -# application/vnd.etsi.tsl+xml -# application/vnd.etsi.tsl.der -# application/vnd.eudora.data -application/vnd.ezpix-album ez2 -application/vnd.ezpix-package ez3 -# application/vnd.f-secure.mobile -application/vnd.fdf fdf -application/vnd.fdsn.mseed mseed -application/vnd.fdsn.seed seed dataless -# application/vnd.ffsns -# application/vnd.fints -application/vnd.flographit gph -application/vnd.fluxtime.clip ftc -# application/vnd.font-fontforge-sfd -application/vnd.framemaker fm frame maker book -application/vnd.frogans.fnc fnc -application/vnd.frogans.ltf ltf -application/vnd.fsc.weblaunch fsc -application/vnd.fujitsu.oasys oas -application/vnd.fujitsu.oasys2 oa2 -application/vnd.fujitsu.oasys3 oa3 -application/vnd.fujitsu.oasysgp fg5 -application/vnd.fujitsu.oasysprs bh2 -# application/vnd.fujixerox.art-ex -# application/vnd.fujixerox.art4 -# application/vnd.fujixerox.hbpl -application/vnd.fujixerox.ddd ddd -application/vnd.fujixerox.docuworks xdw -application/vnd.fujixerox.docuworks.binder xbd -# application/vnd.fut-misnet -application/vnd.fuzzysheet fzs -application/vnd.genomatix.tuxedo txd -# application/vnd.geocube+xml -application/vnd.geogebra.file ggb -application/vnd.geogebra.tool ggt -application/vnd.geometry-explorer gex gre -application/vnd.geonext gxt -application/vnd.geoplan g2w -application/vnd.geospace g3w -# application/vnd.globalplatform.card-content-mgt -# application/vnd.globalplatform.card-content-mgt-response -application/vnd.gmx gmx -application/vnd.google-earth.kml+xml kml -application/vnd.google-earth.kmz kmz -application/vnd.grafeq gqf gqs -# application/vnd.gridmp -application/vnd.groove-account gac -application/vnd.groove-help ghf -application/vnd.groove-identity-message gim -application/vnd.groove-injector grv -application/vnd.groove-tool-message gtm -application/vnd.groove-tool-template tpl -application/vnd.groove-vcard vcg -# application/vnd.hal+json -application/vnd.hal+xml hal -application/vnd.handheld-entertainment+xml zmm -application/vnd.hbci hbci -# application/vnd.hcl-bireports -application/vnd.hhe.lesson-player les -application/vnd.hp-hpgl hpgl -application/vnd.hp-hpid hpid -application/vnd.hp-hps hps -application/vnd.hp-jlyt jlt -application/vnd.hp-pcl pcl -application/vnd.hp-pclxl pclxl -# application/vnd.httphone -application/vnd.hydrostatix.sof-data sfd-hdstx -application/vnd.hzn-3d-crossword x3d -# application/vnd.ibm.afplinedata -# application/vnd.ibm.electronic-media -application/vnd.ibm.minipay mpy -application/vnd.ibm.modcap afp listafp list3820 -application/vnd.ibm.rights-management irm -application/vnd.ibm.secure-container sc -application/vnd.iccprofile icc icm -application/vnd.igloader igl -application/vnd.immervision-ivp ivp -application/vnd.immervision-ivu ivu -# application/vnd.informedcontrol.rms+xml -# application/vnd.informix-visionary -# application/vnd.infotech.project -# application/vnd.infotech.project+xml -application/vnd.insors.igm igm -application/vnd.intercon.formnet xpw xpx -application/vnd.intergeo i2g -# application/vnd.intertrust.digibox -# application/vnd.intertrust.nncp -application/vnd.intu.qbo qbo -application/vnd.intu.qfx qfx -# application/vnd.iptc.g2.conceptitem+xml -# application/vnd.iptc.g2.knowledgeitem+xml -# application/vnd.iptc.g2.newsitem+xml -# application/vnd.iptc.g2.packageitem+xml -application/vnd.ipunplugged.rcprofile rcprofile -application/vnd.irepository.package+xml irp -application/vnd.is-xpr xpr -application/vnd.isac.fcs fcs -application/vnd.jam jam -# application/vnd.japannet-directory-service -# application/vnd.japannet-jpnstore-wakeup -# application/vnd.japannet-payment-wakeup -# application/vnd.japannet-registration -# application/vnd.japannet-registration-wakeup -# application/vnd.japannet-setstore-wakeup -# application/vnd.japannet-verification -# application/vnd.japannet-verification-wakeup -application/vnd.jcp.javame.midlet-rms rms -application/vnd.jisp jisp -application/vnd.joost.joda-archive joda -application/vnd.kahootz ktz ktr -application/vnd.kde.karbon karbon -application/vnd.kde.kchart chrt -application/vnd.kde.kformula kfo -application/vnd.kde.kivio flw -application/vnd.kde.kontour kon -application/vnd.kde.kpresenter kpr kpt -application/vnd.kde.kspread ksp -application/vnd.kde.kword kwd kwt -application/vnd.kenameaapp htke -application/vnd.kidspiration kia -application/vnd.kinar kne knp -application/vnd.koan skp skd skt skm -application/vnd.kodak-descriptor sse -application/vnd.las.las+xml lasxml -# application/vnd.liberty-request+xml -application/vnd.llamagraphics.life-balance.desktop lbd -application/vnd.llamagraphics.life-balance.exchange+xml lbe -application/vnd.lotus-1-2-3 123 -application/vnd.lotus-approach apr -application/vnd.lotus-freelance pre -application/vnd.lotus-notes nsf -application/vnd.lotus-organizer org -application/vnd.lotus-screencam scm -application/vnd.lotus-wordpro lwp -application/vnd.macports.portpkg portpkg -# application/vnd.marlin.drm.actiontoken+xml -# application/vnd.marlin.drm.conftoken+xml -# application/vnd.marlin.drm.license+xml -# application/vnd.marlin.drm.mdcf -application/vnd.mcd mcd -application/vnd.medcalcdata mc1 -application/vnd.mediastation.cdkey cdkey -# application/vnd.meridian-slingshot -application/vnd.mfer mwf -application/vnd.mfmp mfm -application/vnd.micrografx.flo flo -application/vnd.micrografx.igx igx -application/vnd.mif mif -# application/vnd.minisoft-hp3000-save -# application/vnd.mitsubishi.misty-guard.trustweb -application/vnd.mobius.daf daf -application/vnd.mobius.dis dis -application/vnd.mobius.mbk mbk -application/vnd.mobius.mqy mqy -application/vnd.mobius.msl msl -application/vnd.mobius.plc plc -application/vnd.mobius.txf txf -application/vnd.mophun.application mpn -application/vnd.mophun.certificate mpc -# application/vnd.motorola.flexsuite -# application/vnd.motorola.flexsuite.adsi -# application/vnd.motorola.flexsuite.fis -# application/vnd.motorola.flexsuite.gotap -# application/vnd.motorola.flexsuite.kmr -# application/vnd.motorola.flexsuite.ttc -# application/vnd.motorola.flexsuite.wem -# application/vnd.motorola.iprm -application/vnd.mozilla.xul+xml xul -application/vnd.ms-artgalry cil -# application/vnd.ms-asf -application/vnd.ms-cab-compressed cab -application/vnd.ms-excel xls xlm xla xlc xlt xlw -application/vnd.ms-excel.addin.macroenabled.12 xlam -application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb -application/vnd.ms-excel.sheet.macroenabled.12 xlsm -application/vnd.ms-excel.template.macroenabled.12 xltm -application/vnd.ms-fontobject eot -application/vnd.ms-htmlhelp chm -application/vnd.ms-ims ims -application/vnd.ms-lrm lrm -# application/vnd.ms-office.activex+xml -application/vnd.ms-officetheme thmx -application/vnd.ms-pki.seccat cat -application/vnd.ms-pki.stl stl -# application/vnd.ms-playready.initiator+xml -application/vnd.ms-powerpoint ppt pps pot -application/vnd.ms-powerpoint.addin.macroenabled.12 ppam -application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm -application/vnd.ms-powerpoint.slide.macroenabled.12 sldm -application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm -application/vnd.ms-powerpoint.template.macroenabled.12 potm -application/vnd.ms-project mpp mpt -# application/vnd.ms-tnef -# application/vnd.ms-wmdrm.lic-chlg-req -# application/vnd.ms-wmdrm.lic-resp -# application/vnd.ms-wmdrm.meter-chlg-req -# application/vnd.ms-wmdrm.meter-resp -application/vnd.ms-word.document.macroenabled.12 docm -application/vnd.ms-word.template.macroenabled.12 dotm -application/vnd.ms-works wps wks wcm wdb -application/vnd.ms-wpl wpl -application/vnd.ms-xpsdocument xps -application/vnd.mseq mseq -# application/vnd.msign -# application/vnd.multiad.creator -# application/vnd.multiad.creator.cif -# application/vnd.music-niff -application/vnd.musician mus -application/vnd.muvee.style msty -application/vnd.mynfc taglet -# application/vnd.ncd.control -# application/vnd.ncd.reference -# application/vnd.nervana -# application/vnd.netfpx -application/vnd.neurolanguage.nlu nlu -application/vnd.noblenet-directory nnd -application/vnd.noblenet-sealer nns -application/vnd.noblenet-web nnw -# application/vnd.nokia.catalogs -# application/vnd.nokia.conml+wbxml -# application/vnd.nokia.conml+xml -# application/vnd.nokia.isds-radio-presets -# application/vnd.nokia.iptv.config+xml -# application/vnd.nokia.landmark+wbxml -# application/vnd.nokia.landmark+xml -# application/vnd.nokia.landmarkcollection+xml -# application/vnd.nokia.n-gage.ac+xml -application/vnd.nokia.n-gage.data ngdat -application/vnd.nokia.n-gage.symbian.install n-gage -# application/vnd.nokia.ncd -# application/vnd.nokia.pcd+wbxml -# application/vnd.nokia.pcd+xml -application/vnd.nokia.radio-preset rpst -application/vnd.nokia.radio-presets rpss -application/vnd.novadigm.edm edm -application/vnd.novadigm.edx edx -application/vnd.novadigm.ext ext -# application/vnd.ntt-local.file-transfer -# application/vnd.ntt-local.sip-ta_remote -# application/vnd.ntt-local.sip-ta_tcp_stream -application/vnd.oasis.opendocument.chart odc -application/vnd.oasis.opendocument.chart-template otc -application/vnd.oasis.opendocument.database odb -application/vnd.oasis.opendocument.formula odf -application/vnd.oasis.opendocument.formula-template odft -application/vnd.oasis.opendocument.graphics odg -application/vnd.oasis.opendocument.graphics-template otg -application/vnd.oasis.opendocument.image odi -application/vnd.oasis.opendocument.image-template oti -application/vnd.oasis.opendocument.presentation odp -application/vnd.oasis.opendocument.presentation-template otp -application/vnd.oasis.opendocument.spreadsheet ods -application/vnd.oasis.opendocument.spreadsheet-template ots -application/vnd.oasis.opendocument.text odt -application/vnd.oasis.opendocument.text-master odm -application/vnd.oasis.opendocument.text-template ott -application/vnd.oasis.opendocument.text-web oth -# application/vnd.obn -# application/vnd.oftn.l10n+json -# application/vnd.oipf.contentaccessdownload+xml -# application/vnd.oipf.contentaccessstreaming+xml -# application/vnd.oipf.cspg-hexbinary -# application/vnd.oipf.dae.svg+xml -# application/vnd.oipf.dae.xhtml+xml -# application/vnd.oipf.mippvcontrolmessage+xml -# application/vnd.oipf.pae.gem -# application/vnd.oipf.spdiscovery+xml -# application/vnd.oipf.spdlist+xml -# application/vnd.oipf.ueprofile+xml -# application/vnd.oipf.userprofile+xml -application/vnd.olpc-sugar xo -# application/vnd.oma-scws-config -# application/vnd.oma-scws-http-request -# application/vnd.oma-scws-http-response -# application/vnd.oma.bcast.associated-procedure-parameter+xml -# application/vnd.oma.bcast.drm-trigger+xml -# application/vnd.oma.bcast.imd+xml -# application/vnd.oma.bcast.ltkm -# application/vnd.oma.bcast.notification+xml -# application/vnd.oma.bcast.provisioningtrigger -# application/vnd.oma.bcast.sgboot -# application/vnd.oma.bcast.sgdd+xml -# application/vnd.oma.bcast.sgdu -# application/vnd.oma.bcast.simple-symbol-container -# application/vnd.oma.bcast.smartcard-trigger+xml -# application/vnd.oma.bcast.sprov+xml -# application/vnd.oma.bcast.stkm -# application/vnd.oma.cab-address-book+xml -# application/vnd.oma.cab-feature-handler+xml -# application/vnd.oma.cab-pcc+xml -# application/vnd.oma.cab-user-prefs+xml -# application/vnd.oma.dcd -# application/vnd.oma.dcdc -application/vnd.oma.dd2+xml dd2 -# application/vnd.oma.drm.risd+xml -# application/vnd.oma.group-usage-list+xml -# application/vnd.oma.pal+xml -# application/vnd.oma.poc.detailed-progress-report+xml -# application/vnd.oma.poc.final-report+xml -# application/vnd.oma.poc.groups+xml -# application/vnd.oma.poc.invocation-descriptor+xml -# application/vnd.oma.poc.optimized-progress-report+xml -# application/vnd.oma.push -# application/vnd.oma.scidm.messages+xml -# application/vnd.oma.xcap-directory+xml -# application/vnd.omads-email+xml -# application/vnd.omads-file+xml -# application/vnd.omads-folder+xml -# application/vnd.omaloc-supl-init -application/vnd.openofficeorg.extension oxt -# application/vnd.openxmlformats-officedocument.custom-properties+xml -# application/vnd.openxmlformats-officedocument.customxmlproperties+xml -# application/vnd.openxmlformats-officedocument.drawing+xml -# application/vnd.openxmlformats-officedocument.drawingml.chart+xml -# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml -# application/vnd.openxmlformats-officedocument.extended-properties+xml -# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml -# application/vnd.openxmlformats-officedocument.presentationml.comments+xml -# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml -application/vnd.openxmlformats-officedocument.presentationml.presentation pptx -# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml -application/vnd.openxmlformats-officedocument.presentationml.slide sldx -# application/vnd.openxmlformats-officedocument.presentationml.slide+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml -application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx -# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml -# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml -# application/vnd.openxmlformats-officedocument.presentationml.tags+xml -application/vnd.openxmlformats-officedocument.presentationml.template potx -# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx -# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml -# application/vnd.openxmlformats-officedocument.theme+xml -# application/vnd.openxmlformats-officedocument.themeoverride+xml -# application/vnd.openxmlformats-officedocument.vmldrawing -# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.document docx -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx -# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml -# application/vnd.openxmlformats-package.core-properties+xml -# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml -# application/vnd.openxmlformats-package.relationships+xml -# application/vnd.quobject-quoxdocument -# application/vnd.osa.netdeploy -application/vnd.osgeo.mapguide.package mgp -# application/vnd.osgi.bundle -application/vnd.osgi.dp dp -# application/vnd.otps.ct-kip+xml -application/vnd.palm pdb pqa oprc -# application/vnd.paos.xml -application/vnd.pawaafile paw -application/vnd.pg.format str -application/vnd.pg.osasli ei6 -# application/vnd.piaccess.application-licence -application/vnd.picsel efif -application/vnd.pmi.widget wg -# application/vnd.poc.group-advertisement+xml -application/vnd.pocketlearn plf -application/vnd.powerbuilder6 pbd -# application/vnd.powerbuilder6-s -# application/vnd.powerbuilder7 -# application/vnd.powerbuilder7-s -# application/vnd.powerbuilder75 -# application/vnd.powerbuilder75-s -# application/vnd.preminet -application/vnd.previewsystems.box box -application/vnd.proteus.magazine mgz -application/vnd.publishare-delta-tree qps -application/vnd.pvi.ptid1 ptid -# application/vnd.pwg-multiplexed -# application/vnd.pwg-xhtml-print+xml -# application/vnd.qualcomm.brew-app-res -application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb -# application/vnd.radisys.moml+xml -# application/vnd.radisys.msml+xml -# application/vnd.radisys.msml-audit+xml -# application/vnd.radisys.msml-audit-conf+xml -# application/vnd.radisys.msml-audit-conn+xml -# application/vnd.radisys.msml-audit-dialog+xml -# application/vnd.radisys.msml-audit-stream+xml -# application/vnd.radisys.msml-conf+xml -# application/vnd.radisys.msml-dialog+xml -# application/vnd.radisys.msml-dialog-base+xml -# application/vnd.radisys.msml-dialog-fax-detect+xml -# application/vnd.radisys.msml-dialog-fax-sendrecv+xml -# application/vnd.radisys.msml-dialog-group+xml -# application/vnd.radisys.msml-dialog-speech+xml -# application/vnd.radisys.msml-dialog-transform+xml -# application/vnd.rainstor.data -# application/vnd.rapid -application/vnd.realvnc.bed bed -application/vnd.recordare.musicxml mxl -application/vnd.recordare.musicxml+xml musicxml -# application/vnd.renlearn.rlprint -application/vnd.rig.cryptonote cryptonote -application/vnd.rim.cod cod -application/vnd.rn-realmedia rm -application/vnd.route66.link66+xml link66 -# application/vnd.ruckus.download -# application/vnd.s3sms -application/vnd.sailingtracker.track st -# application/vnd.sbm.cid -# application/vnd.sbm.mid2 -# application/vnd.scribus -# application/vnd.sealed.3df -# application/vnd.sealed.csf -# application/vnd.sealed.doc -# application/vnd.sealed.eml -# application/vnd.sealed.mht -# application/vnd.sealed.net -# application/vnd.sealed.ppt -# application/vnd.sealed.tiff -# application/vnd.sealed.xls -# application/vnd.sealedmedia.softseal.html -# application/vnd.sealedmedia.softseal.pdf -application/vnd.seemail see -application/vnd.sema sema -application/vnd.semd semd -application/vnd.semf semf -application/vnd.shana.informed.formdata ifm -application/vnd.shana.informed.formtemplate itp -application/vnd.shana.informed.interchange iif -application/vnd.shana.informed.package ipk -application/vnd.simtech-mindmapper twd twds -application/vnd.smaf mmf -# application/vnd.smart.notebook -application/vnd.smart.teacher teacher -# application/vnd.software602.filler.form+xml -# application/vnd.software602.filler.form-xml-zip -application/vnd.solent.sdkm+xml sdkm sdkd -application/vnd.spotfire.dxp dxp -application/vnd.spotfire.sfs sfs -# application/vnd.sss-cod -# application/vnd.sss-dtf -# application/vnd.sss-ntf -application/vnd.stardivision.calc sdc -application/vnd.stardivision.draw sda -application/vnd.stardivision.impress sdd -application/vnd.stardivision.math smf -application/vnd.stardivision.writer sdw vor -application/vnd.stardivision.writer-global sgl -application/vnd.stepmania.package smzip -application/vnd.stepmania.stepchart sm -# application/vnd.street-stream -application/vnd.sun.xml.calc sxc -application/vnd.sun.xml.calc.template stc -application/vnd.sun.xml.draw sxd -application/vnd.sun.xml.draw.template std -application/vnd.sun.xml.impress sxi -application/vnd.sun.xml.impress.template sti -application/vnd.sun.xml.math sxm -application/vnd.sun.xml.writer sxw -application/vnd.sun.xml.writer.global sxg -application/vnd.sun.xml.writer.template stw -# application/vnd.sun.wadl+xml -application/vnd.sus-calendar sus susp -application/vnd.svd svd -# application/vnd.swiftview-ics -application/vnd.symbian.install sis sisx -application/vnd.syncml+xml xsm -application/vnd.syncml.dm+wbxml bdm -application/vnd.syncml.dm+xml xdm -# application/vnd.syncml.dm.notification -# application/vnd.syncml.ds.notification -application/vnd.tao.intent-module-archive tao -application/vnd.tcpdump.pcap pcap cap dmp -application/vnd.tmobile-livetv tmo -application/vnd.trid.tpt tpt -application/vnd.triscape.mxs mxs -application/vnd.trueapp tra -# application/vnd.truedoc -# application/vnd.ubisoft.webplayer -application/vnd.ufdl ufd ufdl -application/vnd.uiq.theme utz -application/vnd.umajin umj -application/vnd.unity unityweb -application/vnd.uoml+xml uoml -# application/vnd.uplanet.alert -# application/vnd.uplanet.alert-wbxml -# application/vnd.uplanet.bearer-choice -# application/vnd.uplanet.bearer-choice-wbxml -# application/vnd.uplanet.cacheop -# application/vnd.uplanet.cacheop-wbxml -# application/vnd.uplanet.channel -# application/vnd.uplanet.channel-wbxml -# application/vnd.uplanet.list -# application/vnd.uplanet.list-wbxml -# application/vnd.uplanet.listcmd -# application/vnd.uplanet.listcmd-wbxml -# application/vnd.uplanet.signal -application/vnd.vcx vcx -# application/vnd.vd-study -# application/vnd.vectorworks -# application/vnd.verimatrix.vcas -# application/vnd.vidsoft.vidconference -application/vnd.visio vsd vst vss vsw -application/vnd.visionary vis -# application/vnd.vividence.scriptfile -application/vnd.vsf vsf -# application/vnd.wap.sic -# application/vnd.wap.slc -application/vnd.wap.wbxml wbxml -application/vnd.wap.wmlc wmlc -application/vnd.wap.wmlscriptc wmlsc -application/vnd.webturbo wtb -# application/vnd.wfa.wsc -# application/vnd.wmc -# application/vnd.wmf.bootstrap -# application/vnd.wolfram.mathematica -# application/vnd.wolfram.mathematica.package -application/vnd.wolfram.player nbp -application/vnd.wordperfect wpd -application/vnd.wqd wqd -# application/vnd.wrq-hp3000-labelled -application/vnd.wt.stf stf -# application/vnd.wv.csp+wbxml -# application/vnd.wv.csp+xml -# application/vnd.wv.ssp+xml -application/vnd.xara xar -application/vnd.xfdl xfdl -# application/vnd.xfdl.webform -# application/vnd.xmi+xml -# application/vnd.xmpie.cpkg -# application/vnd.xmpie.dpkg -# application/vnd.xmpie.plan -# application/vnd.xmpie.ppkg -# application/vnd.xmpie.xlim -application/vnd.yamaha.hv-dic hvd -application/vnd.yamaha.hv-script hvs -application/vnd.yamaha.hv-voice hvp -application/vnd.yamaha.openscoreformat osf -application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg -# application/vnd.yamaha.remote-setup -application/vnd.yamaha.smaf-audio saf -application/vnd.yamaha.smaf-phrase spf -# application/vnd.yamaha.through-ngn -# application/vnd.yamaha.tunnel-udpencap -application/vnd.yellowriver-custom-menu cmp -application/vnd.zul zir zirz -application/vnd.zzazz.deck+xml zaz -application/voicexml+xml vxml -# application/vq-rtcpxr -# application/watcherinfo+xml -# application/whoispp-query -# application/whoispp-response -application/widget wgt -application/winhlp hlp -# application/wita -# application/wordperfect5.1 -application/wsdl+xml wsdl -application/wspolicy+xml wspolicy -application/x-7z-compressed 7z -application/x-abiword abw -application/x-ace-compressed ace -application/x-authorware-bin aab x32 u32 vox -application/x-authorware-map aam -application/x-authorware-seg aas -application/x-bcpio bcpio -application/x-bittorrent torrent -application/x-bzip bz -application/x-bzip2 bz2 boz -application/x-cdlink vcd -application/x-chat chat -application/x-chess-pgn pgn -# application/x-compress -application/x-cpio cpio -application/x-csh csh -application/x-debian-package deb udeb -application/x-director dir dcr dxr cst cct cxt w3d fgd swa -application/x-doom wad -application/x-dtbncx+xml ncx -application/x-dtbook+xml dtb -application/x-dtbresource+xml res -application/x-dvi dvi -application/x-font-bdf bdf -# application/x-font-dos -# application/x-font-framemaker -application/x-font-ghostscript gsf -# application/x-font-libgrx -application/x-font-linux-psf psf -application/x-font-otf otf -application/x-font-pcf pcf -application/x-font-snf snf -# application/x-font-speedo -# application/x-font-sunos-news -application/x-font-ttf ttf ttc -application/x-font-type1 pfa pfb pfm afm -application/x-font-woff woff -# application/x-font-vfont -application/x-futuresplash spl -application/x-gnumeric gnumeric -application/x-gtar gtar -# application/x-gzip -application/x-hdf hdf -application/x-java-jnlp-file jnlp -application/x-latex latex -application/x-mobipocket-ebook prc mobi -application/x-ms-application application -application/x-ms-wmd wmd -application/x-ms-wmz wmz -application/x-ms-xbap xbap -application/x-msaccess mdb -application/x-msbinder obd -application/x-mscardfile crd -application/x-msclip clp -application/x-msdownload exe dll com bat msi -application/x-msmediaview mvb m13 m14 -application/x-msmetafile wmf -application/x-msmoney mny -application/x-mspublisher pub -application/x-msschedule scd -application/x-msterminal trm -application/x-mswrite wri -application/x-netcdf nc cdf -application/x-pkcs12 p12 pfx -application/x-pkcs7-certificates p7b spc -application/x-pkcs7-certreqresp p7r -application/x-rar-compressed rar -application/x-sh sh -application/x-shar shar -application/x-shockwave-flash swf -application/x-silverlight-app xap -application/x-stuffit sit -application/x-stuffitx sitx -application/x-sv4cpio sv4cpio -application/x-sv4crc sv4crc -application/x-tar tar -application/x-tcl tcl -application/x-tex tex -application/x-tex-tfm tfm -application/x-texinfo texinfo texi -application/x-ustar ustar -application/x-wais-source src -application/x-x509-ca-cert der crt -application/x-xfig fig -application/x-xpinstall xpi -# application/x400-bp -# application/xcap-att+xml -# application/xcap-caps+xml -application/xcap-diff+xml xdf -# application/xcap-el+xml -# application/xcap-error+xml -# application/xcap-ns+xml -# application/xcon-conference-info-diff+xml -# application/xcon-conference-info+xml -application/xenc+xml xenc -application/xhtml+xml xhtml xht -# application/xhtml-voice+xml -application/xml xml xsl -application/xml-dtd dtd -# application/xml-external-parsed-entity -# application/xmpp+xml -application/xop+xml xop -application/xslt+xml xslt -application/xspf+xml xspf -application/xv+xml mxml xhvml xvml xvm -application/yang yang -application/yin+xml yin -application/zip zip -# audio/1d-interleaved-parityfec -# audio/32kadpcm -# audio/3gpp -# audio/3gpp2 -# audio/ac3 -audio/adpcm adp -# audio/amr -# audio/amr-wb -# audio/amr-wb+ -# audio/asc -# audio/atrac-advanced-lossless -# audio/atrac-x -# audio/atrac3 -audio/basic au snd -# audio/bv16 -# audio/bv32 -# audio/clearmode -# audio/cn -# audio/dat12 -# audio/dls -# audio/dsr-es201108 -# audio/dsr-es202050 -# audio/dsr-es202211 -# audio/dsr-es202212 -# audio/dv -# audio/dvi4 -# audio/eac3 -# audio/evrc -# audio/evrc-qcp -# audio/evrc0 -# audio/evrc1 -# audio/evrcb -# audio/evrcb0 -# audio/evrcb1 -# audio/evrcwb -# audio/evrcwb0 -# audio/evrcwb1 -# audio/example -# audio/fwdred -# audio/g719 -# audio/g722 -# audio/g7221 -# audio/g723 -# audio/g726-16 -# audio/g726-24 -# audio/g726-32 -# audio/g726-40 -# audio/g728 -# audio/g729 -# audio/g7291 -# audio/g729d -# audio/g729e -# audio/gsm -# audio/gsm-efr -# audio/gsm-hr-08 -# audio/ilbc -# audio/ip-mr_v2.5 -# audio/l16 -# audio/l20 -# audio/l24 -# audio/l8 -# audio/lpc -audio/midi mid midi kar rmi -# audio/mobile-xmf -audio/mp4 mp4a -# audio/mp4a-latm -# audio/mpa -# audio/mpa-robust -audio/mpeg mpga mp2 mp2a mp3 m2a m3a -# audio/mpeg4-generic -audio/ogg oga ogg spx -# audio/parityfec -# audio/pcma -# audio/pcma-wb -# audio/pcmu-wb -# audio/pcmu -# audio/prs.sid -# audio/qcelp -# audio/red -# audio/rtp-enc-aescm128 -# audio/rtp-midi -# audio/rtx -# audio/smv -# audio/smv0 -# audio/smv-qcp -# audio/sp-midi -# audio/speex -# audio/t140c -# audio/t38 -# audio/telephone-event -# audio/tone -# audio/uemclip -# audio/ulpfec -# audio/vdvi -# audio/vmr-wb -# audio/vnd.3gpp.iufp -# audio/vnd.4sb -# audio/vnd.audiokoz -# audio/vnd.celp -# audio/vnd.cisco.nse -# audio/vnd.cmles.radio-events -# audio/vnd.cns.anp1 -# audio/vnd.cns.inf1 -audio/vnd.dece.audio uva uvva -audio/vnd.digital-winds eol -# audio/vnd.dlna.adts -# audio/vnd.dolby.heaac.1 -# audio/vnd.dolby.heaac.2 -# audio/vnd.dolby.mlp -# audio/vnd.dolby.mps -# audio/vnd.dolby.pl2 -# audio/vnd.dolby.pl2x -# audio/vnd.dolby.pl2z -# audio/vnd.dolby.pulse.1 -audio/vnd.dra dra -audio/vnd.dts dts -audio/vnd.dts.hd dtshd -# audio/vnd.dvb.file dvb -# audio/vnd.everad.plj -# audio/vnd.hns.audio -audio/vnd.lucent.voice lvp -audio/vnd.ms-playready.media.pya pya -# audio/vnd.nokia.mobile-xmf -# audio/vnd.nortel.vbk -audio/vnd.nuera.ecelp4800 ecelp4800 -audio/vnd.nuera.ecelp7470 ecelp7470 -audio/vnd.nuera.ecelp9600 ecelp9600 -# audio/vnd.octel.sbc -# audio/vnd.qcelp -# audio/vnd.rhetorex.32kadpcm -audio/vnd.rip rip -# audio/vnd.sealedmedia.softseal.mpeg -# audio/vnd.vmx.cvsd -# audio/vorbis -# audio/vorbis-config -audio/webm weba -audio/x-aac aac -audio/x-aiff aif aiff aifc -audio/x-mpegurl m3u -audio/x-ms-wax wax -audio/x-ms-wma wma -audio/x-pn-realaudio ram ra -audio/x-pn-realaudio-plugin rmp -audio/x-wav wav -chemical/x-cdx cdx -chemical/x-cif cif -chemical/x-cmdf cmdf -chemical/x-cml cml -chemical/x-csml csml -# chemical/x-pdb -chemical/x-xyz xyz -image/bmp bmp -image/cgm cgm -# image/example -# image/fits -image/g3fax g3 -image/gif gif -image/ief ief -# image/jp2 -image/jpeg jpeg jpg jpe -# image/jpm -# image/jpx -image/ktx ktx -# image/naplps -image/png png -image/prs.btif btif -# image/prs.pti -image/svg+xml svg svgz -# image/t38 -image/tiff tiff tif -# image/tiff-fx -image/vnd.adobe.photoshop psd -# image/vnd.cns.inf2 -image/vnd.dece.graphic uvi uvvi uvg uvvg -image/vnd.dvb.subtitle sub -image/vnd.djvu djvu djv -image/vnd.dwg dwg -image/vnd.dxf dxf -image/vnd.fastbidsheet fbs -image/vnd.fpx fpx -image/vnd.fst fst -image/vnd.fujixerox.edmics-mmr mmr -image/vnd.fujixerox.edmics-rlc rlc -# image/vnd.globalgraphics.pgb -# image/vnd.microsoft.icon -# image/vnd.mix -image/vnd.ms-modi mdi -image/vnd.net-fpx npx -# image/vnd.radiance -# image/vnd.sealed.png -# image/vnd.sealedmedia.softseal.gif -# image/vnd.sealedmedia.softseal.jpg -# image/vnd.svf -image/vnd.wap.wbmp wbmp -image/vnd.xiff xif -image/webp webp -image/x-cmu-raster ras -image/x-cmx cmx -image/x-freehand fh fhc fh4 fh5 fh7 -image/x-icon ico -image/x-pcx pcx -image/x-pict pic pct -image/x-portable-anymap pnm -image/x-portable-bitmap pbm -image/x-portable-graymap pgm -image/x-portable-pixmap ppm -image/x-rgb rgb -image/x-xbitmap xbm -image/x-xpixmap xpm -image/x-xwindowdump xwd -# message/cpim -# message/delivery-status -# message/disposition-notification -# message/example -# message/external-body -# message/feedback-report -# message/global -# message/global-delivery-status -# message/global-disposition-notification -# message/global-headers -# message/http -# message/imdn+xml -# message/news -# message/partial -message/rfc822 eml mime -# message/s-http -# message/sip -# message/sipfrag -# message/tracking-status -# message/vnd.si.simp -# model/example -model/iges igs iges -model/mesh msh mesh silo -model/vnd.collada+xml dae -model/vnd.dwf dwf -# model/vnd.flatland.3dml -model/vnd.gdl gdl -# model/vnd.gs-gdl -# model/vnd.gs.gdl -model/vnd.gtw gtw -# model/vnd.moml+xml -model/vnd.mts mts -# model/vnd.parasolid.transmit.binary -# model/vnd.parasolid.transmit.text -model/vnd.vtu vtu -model/vrml wrl vrml -# multipart/alternative -# multipart/appledouble -# multipart/byteranges -# multipart/digest -# multipart/encrypted -# multipart/example -# multipart/form-data -# multipart/header-set -# multipart/mixed -# multipart/parallel -# multipart/related -# multipart/report -# multipart/signed -# multipart/voice-message -# text/1d-interleaved-parityfec -text/calendar ics ifb -text/css css -text/csv csv -# text/directory -# text/dns -# text/ecmascript -# text/enriched -# text/example -# text/fwdred -text/html html htm -# text/javascript -text/n3 n3 -# text/parityfec -text/plain txt text conf def list log in -# text/prs.fallenstein.rst -text/prs.lines.tag dsc -# text/vnd.radisys.msml-basic-layout -# text/red -# text/rfc822-headers -text/richtext rtx -# text/rtf -# text/rtp-enc-aescm128 -# text/rtx -text/sgml sgml sgm -# text/t140 -text/tab-separated-values tsv -text/troff t tr roff man me ms -text/turtle ttl -# text/ulpfec -text/uri-list uri uris urls -text/vcard vcard -# text/vnd.abc -text/vnd.curl curl -text/vnd.curl.dcurl dcurl -text/vnd.curl.scurl scurl -text/vnd.curl.mcurl mcurl -# text/vnd.dmclientscript -text/vnd.dvb.subtitle sub -# text/vnd.esmertec.theme-descriptor -text/vnd.fly fly -text/vnd.fmi.flexstor flx -text/vnd.graphviz gv -text/vnd.in3d.3dml 3dml -text/vnd.in3d.spot spot -# text/vnd.iptc.newsml -# text/vnd.iptc.nitf -# text/vnd.latex-z -# text/vnd.motorola.reflex -# text/vnd.ms-mediapackage -# text/vnd.net2phone.commcenter.command -# text/vnd.si.uricatalogue -text/vnd.sun.j2me.app-descriptor jad -# text/vnd.trolltech.linguist -# text/vnd.wap.si -# text/vnd.wap.sl -text/vnd.wap.wml wml -text/vnd.wap.wmlscript wmls -text/x-asm s asm -text/x-c c cc cxx cpp h hh dic -text/x-fortran f for f77 f90 -text/x-pascal p pas -text/x-java-source java -text/x-setext etx -text/x-uuencode uu -text/x-vcalendar vcs -text/x-vcard vcf -# text/xml -# text/xml-external-parsed-entity -# video/1d-interleaved-parityfec -video/3gpp 3gp -# video/3gpp-tt -video/3gpp2 3g2 -# video/bmpeg -# video/bt656 -# video/celb -# video/dv -# video/example -video/h261 h261 -video/h263 h263 -# video/h263-1998 -# video/h263-2000 -video/h264 h264 -# video/h264-rcdo -# video/h264-svc -video/jpeg jpgv -# video/jpeg2000 -video/jpm jpm jpgm -video/mj2 mj2 mjp2 -# video/mp1s -# video/mp2p -# video/mp2t -video/mp4 mp4 mp4v mpg4 -# video/mp4v-es -video/mpeg mpeg mpg mpe m1v m2v -# video/mpeg4-generic -# video/mpv -# video/nv -video/ogg ogv -# video/parityfec -# video/pointer -video/quicktime qt mov -# video/raw -# video/rtp-enc-aescm128 -# video/rtx -# video/smpte292m -# video/ulpfec -# video/vc1 -# video/vnd.cctv -video/vnd.dece.hd uvh uvvh -video/vnd.dece.mobile uvm uvvm -# video/vnd.dece.mp4 -video/vnd.dece.pd uvp uvvp -video/vnd.dece.sd uvs uvvs -video/vnd.dece.video uvv uvvv -# video/vnd.directv.mpeg -# video/vnd.directv.mpeg-tts -# video/vnd.dlna.mpeg-tts -video/vnd.dvb.file dvb -video/vnd.fvt fvt -# video/vnd.hns.video -# video/vnd.iptvforum.1dparityfec-1010 -# video/vnd.iptvforum.1dparityfec-2005 -# video/vnd.iptvforum.2dparityfec-1010 -# video/vnd.iptvforum.2dparityfec-2005 -# video/vnd.iptvforum.ttsavc -# video/vnd.iptvforum.ttsmpeg2 -# video/vnd.motorola.video -# video/vnd.motorola.videop -video/vnd.mpegurl mxu m4u -video/vnd.ms-playready.media.pyv pyv -# video/vnd.nokia.interleaved-multimedia -# video/vnd.nokia.videovoip -# video/vnd.objectvideo -# video/vnd.sealed.mpeg1 -# video/vnd.sealed.mpeg4 -# video/vnd.sealed.swf -# video/vnd.sealedmedia.softseal.mov -video/vnd.uvvu.mp4 uvu uvvu -video/vnd.vivo viv -video/webm webm -video/x-f4v f4v -video/x-fli fli -video/x-flv flv -video/x-m4v m4v -video/x-ms-asf asf asx -video/x-ms-wm wm -video/x-ms-wmv wmv -video/x-ms-wmx wmx -video/x-ms-wvx wvx -video/x-msvideo avi -video/x-sgi-movie movie -x-conference/x-cooltalk ice diff --git a/svr/lib/types/node.types b/svr/lib/types/node.types deleted file mode 100644 index 69a6641..0000000 --- a/svr/lib/types/node.types +++ /dev/null @@ -1,54 +0,0 @@ -# What: Google Chrome Extension -# Why: To allow apps to (work) be served with the right content type header. -# http://codereview.chromium.org/2830017 -# Added by: niftylettuce -application/x-chrome-extension crx - -# What: OTF Message Silencer -# Why: To silence the "Resource interpreted as font but transferred with MIME -# type font/otf" message that occurs in Google Chrome -# Added by: niftylettuce -font/opentype otf - -# What: HTC support -# Why: To properly render .htc files such as CSS3PIE -# Added by: niftylettuce -text/x-component htc - -# What: HTML5 application cache manifest -# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps -# per https://developer.mozilla.org/en/offline_resources_in_firefox -# Added by: louisremi -text/cache-manifest appcache manifest - -# What: node binary buffer format -# Why: semi-standard extension w/in the node community -# Added by: tootallnate -application/octet-stream buffer - -# What: The "protected" MP-4 formats used by iTunes. -# Why: Required for streaming music to browsers (?) -# Added by: broofa -application/mp4 m4p -audio/mp4 m4a - -# What: Music playlist format (http://en.wikipedia.org/wiki/M3U) -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -application/x-mpegURL m3u8 - -# What: Video format, Part of RFC1890 -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -video/MP2T ts - -# What: The FLAC lossless codec format -# Why: Streaming and serving FLAC audio -# Added by: jacobrask -audio/flac flac - -# What: EventSource mime type -# Why: mime type of Server-Sent Events stream -# http://www.w3.org/TR/eventsource/#text-event-stream -# Added by: francois2metz -text/event-stream event-stream diff --git a/svr/lib/underscore.js b/svr/lib/underscore.js deleted file mode 100644 index 1ebe267..0000000 --- a/svr/lib/underscore.js +++ /dev/null @@ -1,1200 +0,0 @@ -// Underscore.js 1.4.2 -// http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore may be freely distributed under the MIT license. - -(function() { - - // Baseline setup - // -------------- - - // Establish the root object, `window` in the browser, or `global` on the server. - var root = this; - - // Save the previous value of the `_` variable. - var previousUnderscore = root._; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - // Save bytes in the minified (but not gzipped) version: - var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; - - // Create quick reference variables for speed access to core prototypes. - var push = ArrayProto.push, - slice = ArrayProto.slice, - concat = ArrayProto.concat, - unshift = ArrayProto.unshift, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = function(obj) { - if (obj instanceof _) return obj; - if (!(this instanceof _)) return new _(obj); - this._wrapped = obj; - }; - - // Export the Underscore object for **Node.js**, with - // backwards-compatibility for the old `require()` API. If we're in - // the browser, add `_` as a global object via a string identifier, - // for Closure Compiler "advanced" mode. - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = _; - } - exports._ = _; - } else { - root['_'] = _; - } - - // Current version. - _.VERSION = '1.4.2'; - - // Collection Functions - // -------------------- - - // The cornerstone, an `each` implementation, aka `forEach`. - // Handles objects with the built-in `forEach`, arrays, and raw objects. - // Delegates to **ECMAScript 5**'s native `forEach` if available. - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - // Return the results of applying the iterator to each element. - // Delegates to **ECMAScript 5**'s native `map` if available. - _.map = _.collect = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); - each(obj, function(value, index, list) { - results[results.length] = iterator.call(context, value, index, list); - }); - return results; - }; - - // **Reduce** builds up a single result from a list of values, aka `inject`, - // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. - _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduce && obj.reduce === nativeReduce) { - if (context) iterator = _.bind(iterator, context); - return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); - } - each(obj, function(value, index, list) { - if (!initial) { - memo = value; - initial = true; - } else { - memo = iterator.call(context, memo, value, index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // The right-associative version of reduce, also known as `foldr`. - // Delegates to **ECMAScript 5**'s native `reduceRight` if available. - _.reduceRight = _.foldr = function(obj, iterator, memo, context) { - var initial = arguments.length > 2; - if (obj == null) obj = []; - if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { - if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); - } - var length = obj.length; - if (length !== +length) { - var keys = _.keys(obj); - length = keys.length; - } - each(obj, function(value, index, list) { - index = keys ? keys[--length] : --length; - if (!initial) { - memo = obj[index]; - initial = true; - } else { - memo = iterator.call(context, memo, obj[index], index, list); - } - }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); - return memo; - }; - - // Return the first value which passes a truth test. Aliased as `detect`. - _.find = _.detect = function(obj, iterator, context) { - var result; - any(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) { - result = value; - return true; - } - }); - return result; - }; - - // Return all the elements that pass a truth test. - // Delegates to **ECMAScript 5**'s native `filter` if available. - // Aliased as `select`. - _.filter = _.select = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); - each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Return all the elements for which a truth test fails. - _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Determine whether all of the elements match a truth test. - // Delegates to **ECMAScript 5**'s native `every` if available. - // Aliased as `all`. - _.every = _.all = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = true; - if (obj == null) return result; - if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); - each(obj, function(value, index, list) { - if (!(result = result && iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if at least one element in the object matches a truth test. - // Delegates to **ECMAScript 5**'s native `some` if available. - // Aliased as `any`. - var any = _.some = _.any = function(obj, iterator, context) { - iterator || (iterator = _.identity); - var result = false; - if (obj == null) return result; - if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); - each(obj, function(value, index, list) { - if (result || (result = iterator.call(context, value, index, list))) return breaker; - }); - return !!result; - }; - - // Determine if the array or object contains a given value (using `===`). - // Aliased as `include`. - _.contains = _.include = function(obj, target) { - var found = false; - if (obj == null) return found; - if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { - return value === target; - }); - return found; - }; - - // Invoke a method (with arguments) on every item in a collection. - _.invoke = function(obj, method) { - var args = slice.call(arguments, 2); - return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); - }); - }; - - // Convenience version of a common use case of `map`: fetching a property. - _.pluck = function(obj, key) { - return _.map(obj, function(value){ return value[key]; }); - }; - - // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { - for (var key in attrs) { - if (attrs[key] !== value[key]) return false; - } - return true; - }); - }; - - // Return the maximum element or (element-based computation). - // Can't optimize arrays of integers longer than 65,535 elements. - // See: https://bugs.webkit.org/show_bug.cgi?id=80797 - _.max = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.max.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; - each(obj, function(value, index, list) { - var computed = iterator ? iterator.call(context, value, index, list) : value; - computed >= result.computed && (result = {value : value, computed : computed}); - }); - return result.value; - }; - - // Return the minimum element (or element-based computation). - _.min = function(obj, iterator, context) { - if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { - return Math.min.apply(Math, obj); - } - if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; - each(obj, function(value, index, list) { - var computed = iterator ? iterator.call(context, value, index, list) : value; - computed < result.computed && (result = {value : value, computed : computed}); - }); - return result.value; - }; - - // Shuffle an array. - _.shuffle = function(obj) { - var rand; - var index = 0; - var shuffled = []; - each(obj, function(value) { - rand = _.random(index++); - shuffled[index - 1] = shuffled[rand]; - shuffled[rand] = value; - }); - return shuffled; - }; - - // An internal function to generate lookup iterators. - var lookupIterator = function(value) { - return _.isFunction(value) ? value : function(obj){ return obj[value]; }; - }; - - // Sort the object's values by a criterion produced by an iterator. - _.sortBy = function(obj, value, context) { - var iterator = lookupIterator(value); - return _.pluck(_.map(obj, function(value, index, list) { - return { - value : value, - index : index, - criteria : iterator.call(context, value, index, list) - }; - }).sort(function(left, right) { - var a = left.criteria; - var b = right.criteria; - if (a !== b) { - if (a > b || a === void 0) return 1; - if (a < b || b === void 0) return -1; - } - return left.index < right.index ? -1 : 1; - }), 'value'); - }; - - // An internal function used for aggregate "group by" operations. - var group = function(obj, value, context, behavior) { - var result = {}; - var iterator = lookupIterator(value); - each(obj, function(value, index) { - var key = iterator.call(context, value, index, obj); - behavior(result, key, value); - }); - return result; - }; - - // Groups the object's values by a criterion. Pass either a string attribute - // to group by, or a function that returns the criterion. - _.groupBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - (_.has(result, key) ? result[key] : (result[key] = [])).push(value); - }); - }; - - // Counts instances of an object that group by a certain criterion. Pass - // either a string attribute to count by, or a function that returns the - // criterion. - _.countBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { - if (!_.has(result, key)) result[key] = 0; - result[key]++; - }); - }; - - // Use a comparator function to figure out the smallest index at which - // an object should be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iterator, context) { - iterator = iterator == null ? _.identity : lookupIterator(iterator); - var value = iterator.call(context, obj); - var low = 0, high = array.length; - while (low < high) { - var mid = (low + high) >>> 1; - iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; - } - return low; - }; - - // Safely convert anything iterable into a real, live array. - _.toArray = function(obj) { - if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); - return _.values(obj); - }; - - // Return the number of elements in an object. - _.size = function(obj) { - return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; - }; - - // Array Functions - // --------------- - - // Get the first element of an array. Passing **n** will return the first N - // values in the array. Aliased as `head` and `take`. The **guard** check - // allows it to work with `_.map`. - _.first = _.head = _.take = function(array, n, guard) { - return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; - }; - - // Returns everything but the last entry of the array. Especially useful on - // the arguments object. Passing **n** will return all the values in - // the array, excluding the last N. The **guard** check allows it to work with - // `_.map`. - _.initial = function(array, n, guard) { - return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); - }; - - // Get the last element of an array. Passing **n** will return the last N - // values in the array. The **guard** check allows it to work with `_.map`. - _.last = function(array, n, guard) { - if ((n != null) && !guard) { - return slice.call(array, Math.max(array.length - n, 0)); - } else { - return array[array.length - 1]; - } - }; - - // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. - // Especially useful on the arguments object. Passing an **n** will return - // the rest N values in the array. The **guard** - // check allows it to work with `_.map`. - _.rest = _.tail = _.drop = function(array, n, guard) { - return slice.call(array, (n == null) || guard ? 1 : n); - }; - - // Trim out all falsy values from an array. - _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); - }; - - // Internal implementation of a recursive `flatten` function. - var flatten = function(input, shallow, output) { - each(input, function(value) { - if (_.isArray(value)) { - shallow ? push.apply(output, value) : flatten(value, shallow, output); - } else { - output.push(value); - } - }); - return output; - }; - - // Return a completely flattened version of an array. - _.flatten = function(array, shallow) { - return flatten(array, shallow, []); - }; - - // Return a version of the array that does not contain the specified value(s). - _.without = function(array) { - return _.difference(array, slice.call(arguments, 1)); - }; - - // Produce a duplicate-free version of the array. If the array has already - // been sorted, you have the option of using a faster algorithm. - // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted, iterator, context) { - var initial = iterator ? _.map(array, iterator, context) : array; - var results = []; - var seen = []; - each(initial, function(value, index) { - if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { - seen.push(value); - results.push(array[index]); - } - }); - return results; - }; - - // Produce an array that contains the union: each distinct element from all of - // the passed-in arrays. - _.union = function() { - return _.uniq(concat.apply(ArrayProto, arguments)); - }; - - // Produce an array that contains every item shared between all the - // passed-in arrays. - _.intersection = function(array) { - var rest = slice.call(arguments, 1); - return _.filter(_.uniq(array), function(item) { - return _.every(rest, function(other) { - return _.indexOf(other, item) >= 0; - }); - }); - }; - - // Take the difference between one array and a number of other arrays. - // Only the elements present in just the first array will remain. - _.difference = function(array) { - var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); - return _.filter(array, function(value){ return !_.contains(rest, value); }); - }; - - // Zip together multiple lists into a single array -- elements that share - // an index go together. - _.zip = function() { - var args = slice.call(arguments); - var length = _.max(_.pluck(args, 'length')); - var results = new Array(length); - for (var i = 0; i < length; i++) { - results[i] = _.pluck(args, "" + i); - } - return results; - }; - - // Converts lists into objects. Pass either a single array of `[key, value]` - // pairs, or two parallel arrays of the same length -- one of keys, and one of - // the corresponding values. - _.object = function(list, values) { - var result = {}; - for (var i = 0, l = list.length; i < l; i++) { - if (values) { - result[list[i]] = values[i]; - } else { - result[list[i][0]] = list[i][1]; - } - } - return result; - }; - - // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), - // we need this function. Return the position of the first occurrence of an - // item in an array, or -1 if the item is not included in the array. - // Delegates to **ECMAScript 5**'s native `indexOf` if available. - // If the array is large and already in sort order, pass `true` - // for **isSorted** to use binary search. - _.indexOf = function(array, item, isSorted) { - if (array == null) return -1; - var i = 0, l = array.length; - if (isSorted) { - if (typeof isSorted == 'number') { - i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); - } else { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; - } - } - if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); - for (; i < l; i++) if (array[i] === item) return i; - return -1; - }; - - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. - _.lastIndexOf = function(array, item, from) { - if (array == null) return -1; - var hasIndex = from != null; - if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { - return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); - } - var i = (hasIndex ? from : array.length); - while (i--) if (array[i] === item) return i; - return -1; - }; - - // Generate an integer Array containing an arithmetic progression. A port of - // the native Python `range()` function. See - // [the Python documentation](http://docs.python.org/library/functions.html#range). - _.range = function(start, stop, step) { - if (arguments.length <= 1) { - stop = start || 0; - start = 0; - } - step = arguments[2] || 1; - - var len = Math.max(Math.ceil((stop - start) / step), 0); - var idx = 0; - var range = new Array(len); - - while(idx < len) { - range[idx++] = start; - start += step; - } - - return range; - }; - - // Function (ahem) Functions - // ------------------ - - // Reusable constructor function for prototype setting. - var ctor = function(){}; - - // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - if (!_.isFunction(func)) throw new TypeError; - args = slice.call(arguments, 2); - return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - ctor.prototype = func.prototype; - var self = new ctor; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; - }; - }; - - // Bind all of an object's methods to that object. Useful for ensuring that - // all callbacks defined on an object belong to it. - _.bindAll = function(obj) { - var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); - each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); - return obj; - }; - - // Memoize an expensive function by storing its results. - _.memoize = function(func, hasher) { - var memo = {}; - hasher || (hasher = _.identity); - return function() { - var key = hasher.apply(this, arguments); - return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); - }; - }; - - // Delays a function for the given number of milliseconds, and then calls - // it with the arguments supplied. - _.delay = function(func, wait) { - var args = slice.call(arguments, 2); - return setTimeout(function(){ return func.apply(null, args); }, wait); - }; - - // Defers a function, scheduling it to run after the current call stack has - // cleared. - _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); - }; - - // Returns a function, that, when invoked, will only be triggered at most once - // during a given window of time. - _.throttle = function(func, wait) { - var context, args, timeout, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); - return function() { - context = this; args = arguments; - var later = function() { - timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; - result = func.apply(context, args); - } - whenDone(); - return result; - }; - }; - - // Returns a function, that, as long as it continues to be invoked, will not - // be triggered. The function will be called after it stops being called for - // N milliseconds. If `immediate` is passed, trigger the function on the - // leading edge, instead of the trailing. - _.debounce = function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments; - var later = function() { - timeout = null; - if (!immediate) result = func.apply(context, args); - }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) result = func.apply(context, args); - return result; - }; - }; - - // Returns a function that will be executed at most one time, no matter how - // often you call it. Useful for lazy initialization. - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - memo = func.apply(this, arguments); - func = null; - return memo; - }; - }; - - // Returns the first function passed as an argument to the second, - // allowing you to adjust arguments, run code before and after, and - // conditionally execute the original function. - _.wrap = function(func, wrapper) { - return function() { - var args = [func]; - push.apply(args, arguments); - return wrapper.apply(this, args); - }; - }; - - // Returns a function that is the composition of a list of functions, each - // consuming the return value of the function that follows. - _.compose = function() { - var funcs = arguments; - return function() { - var args = arguments; - for (var i = funcs.length - 1; i >= 0; i--) { - args = [funcs[i].apply(this, args)]; - } - return args[0]; - }; - }; - - // Returns a function that will only be executed after being called N times. - _.after = function(times, func) { - if (times <= 0) return func(); - return function() { - if (--times < 1) { - return func.apply(this, arguments); - } - }; - }; - - // Object Functions - // ---------------- - - // Retrieve the names of an object's properties. - // Delegates to **ECMAScript 5**'s native `Object.keys` - _.keys = nativeKeys || function(obj) { - if (obj !== Object(obj)) throw new TypeError('Invalid object'); - var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; - return keys; - }; - - // Retrieve the values of an object's properties. - _.values = function(obj) { - var values = []; - for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); - return values; - }; - - // Convert an object into a list of `[key, value]` pairs. - _.pairs = function(obj) { - var pairs = []; - for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); - return pairs; - }; - - // Invert the keys and values of an object. The values must be serializable. - _.invert = function(obj) { - var result = {}; - for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; - return result; - }; - - // Return a sorted list of the function names available on the object. - // Aliased as `methods` - _.functions = _.methods = function(obj) { - var names = []; - for (var key in obj) { - if (_.isFunction(obj[key])) names.push(key); - } - return names.sort(); - }; - - // Extend a given object with all the properties in passed-in object(s). - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Return a copy of the object only containing the whitelisted properties. - _.pick = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - each(keys, function(key) { - if (key in obj) copy[key] = obj[key]; - }); - return copy; - }; - - // Return a copy of the object without the blacklisted properties. - _.omit = function(obj) { - var copy = {}; - var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); - for (var key in obj) { - if (!_.contains(keys, key)) copy[key] = obj[key]; - } - return copy; - }; - - // Fill in a given object with default properties. - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - }); - return obj; - }; - - // Create a (shallow-cloned) duplicate of an object. - _.clone = function(obj) { - if (!_.isObject(obj)) return obj; - return _.isArray(obj) ? obj.slice() : _.extend({}, obj); - }; - - // Invokes interceptor with the obj, and then returns obj. - // The primary purpose of this method is to "tap into" a method chain, in - // order to perform operations on intermediate results within the chain. - _.tap = function(obj, interceptor) { - interceptor(obj); - return obj; - }; - - // Internal recursive comparison function for `isEqual`. - var eq = function(a, b, aStack, bStack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. - if (a === b) return a !== 0 || 1 / a == 1 / b; - // A strict comparison is necessary because `null == undefined`. - if (a == null || b == null) return a === b; - // Unwrap any wrapped objects. - if (a instanceof _) a = a._wrapped; - if (b instanceof _) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className != toString.call(b)) return false; - switch (className) { - // Strings, numbers, dates, and booleans are compared by value. - case '[object String]': - // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is - // equivalent to `new String("5")`. - return a == String(b); - case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for - // other numeric values. - return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); - case '[object Date]': - case '[object Boolean]': - // Coerce dates and booleans to numeric primitive values. Dates are compared by their - // millisecond representations. Note that invalid dates with millisecond representations - // of `NaN` are not equivalent. - return +a == +b; - // RegExps are compared by their source patterns and flags. - case '[object RegExp]': - return a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - } - if (typeof a != 'object' || typeof b != 'object') return false; - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] == a) return bStack[length] == b; - } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); - var size = 0, result = true; - // Recursively compare objects and arrays. - if (className == '[object Array]') { - // Compare array lengths to determine if a deep comparison is necessary. - size = a.length; - result = size == b.length; - if (result) { - // Deep compare the contents, ignoring non-numeric properties. - while (size--) { - if (!(result = eq(a[size], b[size], aStack, bStack))) break; - } - } - } else { - // Objects with different constructors are not equivalent, but `Object`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && - _.isFunction(bCtor) && (bCtor instanceof bCtor))) { - return false; - } - // Deep compare objects. - for (var key in a) { - if (_.has(a, key)) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; - } - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (_.has(b, key) && !(size--)) break; - } - result = !size; - } - } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); - return result; - }; - - // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - return eq(a, b, [], []); - }; - - // Is a given array, string, or object empty? - // An "empty" object has no enumerable own-properties. - _.isEmpty = function(obj) { - if (obj == null) return true; - if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; - for (var key in obj) if (_.has(obj, key)) return false; - return true; - }; - - // Is a given value a DOM element? - _.isElement = function(obj) { - return !!(obj && obj.nodeType === 1); - }; - - // Is a given value an array? - // Delegates to ECMA5's native Array.isArray - _.isArray = nativeIsArray || function(obj) { - return toString.call(obj) == '[object Array]'; - }; - - // Is a given variable an object? - _.isObject = function(obj) { - return obj === Object(obj); - }; - - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { - _['is' + name] = function(obj) { - return toString.call(obj) == '[object ' + name + ']'; - }; - }); - - // Define a fallback version of the method in browsers (ahem, IE), where - // there isn't any inspectable "Arguments" type. - if (!_.isArguments(arguments)) { - _.isArguments = function(obj) { - return !!(obj && _.has(obj, 'callee')); - }; - } - - // Optimize `isFunction` if appropriate. - if (typeof (/./) !== 'function') { - _.isFunction = function(obj) { - return typeof obj === 'function'; - }; - } - - // Is a given object a finite number? - _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); - }; - - // Is the given value `NaN`? (NaN is the only number which does not equal itself). - _.isNaN = function(obj) { - return _.isNumber(obj) && obj != +obj; - }; - - // Is a given value a boolean? - _.isBoolean = function(obj) { - return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; - }; - - // Is a given value equal to null? - _.isNull = function(obj) { - return obj === null; - }; - - // Is a given variable undefined? - _.isUndefined = function(obj) { - return obj === void 0; - }; - - // Shortcut function for checking if an object has a given property directly - // on itself (in other words, not on a prototype). - _.has = function(obj, key) { - return hasOwnProperty.call(obj, key); - }; - - // Utility Functions - // ----------------- - - // Run Underscore.js in *noConflict* mode, returning the `_` variable to its - // previous owner. Returns a reference to the Underscore object. - _.noConflict = function() { - root._ = previousUnderscore; - return this; - }; - - // Keep the identity function around for default iterators. - _.identity = function(value) { - return value; - }; - - // Run a function **n** times. - _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); - }; - - // Return a random integer between min and max (inclusive). - _.random = function(min, max) { - if (max == null) { - max = min; - min = 0; - } - return min + (0 | Math.random() * (max - min + 1)); - }; - - // List of HTML entities for escaping. - var entityMap = { - escape: { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - } - }; - entityMap.unescape = _.invert(entityMap.escape); - - // Regexes containing the keys and values listed immediately above. - var entityRegexes = { - escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), - unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') - }; - - // Functions for escaping and unescaping strings to/from HTML interpolation. - _.each(['escape', 'unescape'], function(method) { - _[method] = function(string) { - if (string == null) return ''; - return ('' + string).replace(entityRegexes[method], function(match) { - return entityMap[method][match]; - }); - }; - }); - - // If the value of the named property is a function then invoke it; - // otherwise, return it. - _.result = function(object, property) { - if (object == null) return null; - var value = object[property]; - return _.isFunction(value) ? value.call(object) : value; - }; - - // Add your own custom functions to the Underscore object. - _.mixin = function(obj) { - each(_.functions(obj), function(name){ - var func = _[name] = obj[name]; - _.prototype[name] = function() { - var args = [this._wrapped]; - push.apply(args, arguments); - return result.call(this, func.apply(_, args)); - }; - }); - }; - - // Generate a unique integer id (unique within the entire client session). - // Useful for temporary DOM ids. - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = idCounter++; - return prefix ? prefix + id : id; - }; - - // By default, Underscore uses ERB-style template delimiters, change the - // following template settings to use alternative delimiters. - _.templateSettings = { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g - }; - - // When customizing `templateSettings`, if you don't want to define an - // interpolation, evaluation or escaping regex, we need one that is - // guaranteed not to match. - var noMatch = /(.)^/; - - // Certain characters need to be escaped so that they can be put into a - // string literal. - var escapes = { - "'": "'", - '\\': '\\', - '\r': 'r', - '\n': 'n', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; - - // JavaScript micro-templating, similar to John Resig's implementation. - // Underscore templating handles arbitrary delimiters, preserves whitespace, - // and correctly escapes quotes within interpolated code. - _.template = function(text, data, settings) { - settings = _.defaults({}, settings, _.templateSettings); - - // Combine delimiters into one regular expression via alternation. - var matcher = new RegExp([ - (settings.escape || noMatch).source, - (settings.interpolate || noMatch).source, - (settings.evaluate || noMatch).source - ].join('|') + '|$', 'g'); - - // Compile the template source, escaping string literals appropriately. - var index = 0; - var source = "__p+='"; - text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { - source += text.slice(index, offset) - .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; - index = offset + match.length; - }); - source += "';\n"; - - // If a variable is not specified, place data values in local scope. - if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; - - source = "var __t,__p='',__j=Array.prototype.join," + - "print=function(){__p+=__j.call(arguments,'');};\n" + - source + "return __p;\n"; - - try { - var render = new Function(settings.variable || 'obj', '_', source); - } catch (e) { - e.source = source; - throw e; - } - - if (data) return render(data, _); - var template = function(data) { - return render.call(this, data, _); - }; - - // Provide the compiled function source as a convenience for precompilation. - template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; - - return template; - }; - - // Add a "chain" function, which will delegate to the wrapper. - _.chain = function(obj) { - return _(obj).chain(); - }; - - // OOP - // --------------- - // If Underscore is called as a function, it returns a wrapped object that - // can be used OO-style. This wrapper holds altered versions of all the - // underscore functions. Wrapped objects may be chained. - - // Helper function to continue chaining intermediate results. - var result = function(obj) { - return this._chain ? _(obj).chain() : obj; - }; - - // Add all of the Underscore functions to the wrapper object. - _.mixin(_); - - // Add all mutator Array functions to the wrapper. - each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - var obj = this._wrapped; - method.apply(obj, arguments); - if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; - return result.call(this, obj); - }; - }); - - // Add all accessor Array functions to the wrapper. - each(['concat', 'join', 'slice'], function(name) { - var method = ArrayProto[name]; - _.prototype[name] = function() { - return result.call(this, method.apply(this._wrapped, arguments)); - }; - }); - - _.extend(_.prototype, { - - // Start chaining a wrapped Underscore object. - chain: function() { - this._chain = true; - return this; - }, - - // Extracts the result from a wrapped and chained object. - value: function() { - return this._wrapped; - } - - }); - -}).call(this); diff --git a/svr/lib/util.js b/svr/lib/util.js deleted file mode 100644 index 61f6b4c..0000000 --- a/svr/lib/util.js +++ /dev/null @@ -1,6 +0,0 @@ -// Backwards compatibility ... -try { - module.exports = require('util'); -} catch (e) { - module.exports = require('sys'); -} diff --git a/svr/ref/Math.uuid.js b/svr/ref/Math.uuid.js deleted file mode 100644 index 7538e67..0000000 --- a/svr/ref/Math.uuid.js +++ /dev/null @@ -1,92 +0,0 @@ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); diff --git a/svr/tool/Combine.js b/svr/tool/Combine.js deleted file mode 100644 index fba9156..0000000 --- a/svr/tool/Combine.js +++ /dev/null @@ -1,226 +0,0 @@ -/* -* Description: Combine the files into one, support directory and config files. -* Author: Kris Zhang -* Blog: http://c52u.com -*/ - -//Combine namespace -var Combine; - -(function() { - - var fs = require("fs"), - path = require("path"), - timerID = null; - - var combine = Combine = module.exports = { - sourceFile: "", - targetFile: "", - files: [], //combine list - list: [], //watch list - watch: false, //listen on the changes? - - //Interface - init: function(sourceFile, targetFile, watch) { - //Combine type, it's a directory or cfg file - fs.stat(sourceFile, function(err, stat) { - if (err) { - console.log(err); - return; - } - - combine.sourceFile = sourceFile; - combine.targetFile = targetFile; - combine.watch = watch; - - //It's a configuration files or dictionary - if (stat.isFile()) { - combine.setCfg(sourceFile); - } else { - combine.setDir(sourceFile); - } - }); - }, - - //get output stream - getStream: function() { - var stream; - try { - stream = fs.createWriteStream(combine.targetFile); - } catch (err) { - console.log("Can't create output stream: ", err); - } - return stream; - }, - - //get files from cfgFile, return absolute file path - getFiles: function(cfgPath) { - var contents = fs.readFileSync(cfgPath, 'utf-8'), - files = [], - lastIdx = cfgPath.lastIndexOf('\\'), - dir = cfgPath.substring(0, lastIdx > -1 ? lastIdx : cfgPath.lastIndexOf('/') ); - - //read a file line-by-line - contents.match(/[^\r\n]+/g).forEach(function(line) { - //ignore comments that begin with '#' - if (line[0] != '#') { - files.push(path.join(dir, line)); - } - }); - - combine.watch && files.forEach(function(file) { - if (combine.list.indexOf(file) < 0) { - combine.watchFile(file); - combine.list.push(file); - }; - }); - - combine.files = files; - - return files; - }, - - //Watch changes on source folder - setDir: function(directory) { - //Combine at the first running, then watching the changes. - if (combine.combineDir(directory)) { - combine.watch && fs.watch(directory, function() { - combine.combineDir(directory); - }); - } - }, - - //Watch chagnes on configuration fiel - setCfg: function(configuration) { - var combineCfg = function() { - //get file list from the configuration files. - combine.getFiles(configuration); - combine.combine(); - }; - - //Listen on the change on the configuration file - combine.watch && fs.watch(configuration, combineCfg); - - //combine at the first running - combineCfg(); - }, - - //Watch changes on a file - watchFile: function(file) { - try { - fs.watch(file, function() { - combine.combine(); - }); - } catch (err) { - console.log(file, err); - } - }, - - //Combine directory - combineDir: function(directory) { - try { - var allFiles = fs.readdirSync(directory), - //File name must be consist of numbers characters or "-" "_", "." - fileReg = /^[a-zA-Z0-9-_\.]+$/, - files = []; - - allFiles.forEach(function(file) { - if (fileReg.test(file)) { - files.push(path.join(directory, file)); - } else { - console.log("Skip file:" + file); - } - }); - - combine.files = files; - - return combine.combine(); - } catch (err) { - console.log(err); - return false; - } - }, - - //Combine set of files into one - combine: function() { - - //Prevent other request within 1 seconds; - if (timerID) return; - - timerID = setTimeout(function() { - timerID = null; - }, 1000); - - var oStream = combine.getStream(), r = true; - if (!oStream) { - return false; - } - - var files = combine.files; - - try { - files.forEach(function(file) { - var stat = fs.statSync(file); - - if (!stat.isFile()) { - console.log("Skip folder:" + file); - } else { - var data = fs.readFileSync(file); - oStream.write("/*" + file + "*/\r\n"); - oStream.write(data); - oStream.write("\r\n"); - - console.log("Adding file:" + file); - } - }); - oStream.end(); - - var endTime = new Date(); - console.log("count:", - files.length, - ", date:", new Date().toTimeString(), - "\r\n\r\n" - ); - } catch (err) { - console.log(err); - r = false; - } - - return r; - } - }; - -})(); - - -/* -* call it from command lines -* -i filepath: input directory or cfg file -* -o filepath: output files -* -w: keep watch the changes? -*/ -(function() { - - /* - * parsing parameters from command line - * etc, node combine.js -i configfile.path -o outputfile.path - * the parameter will be: '-' + one character, like: parsing('-o'); - */ - var parsing = function(args, key) { - if (!key || key.length != 2 || key[0] != '-') return; - - var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), - param = args.match(reg); - - if (param && param[0]) { - return param[0].substr(4, 500); - } - }; - - var args = process.argv.join(' '), - input = parsing(args, '-i'), - output = parsing(args, '-o'); - - Combine.init(input, output, args.indexOf(' -w') > 0); - -})(); diff --git a/svr/websvr.js b/svr/websvr.js deleted file mode 100644 index f951480..0000000 --- a/svr/websvr.js +++ /dev/null @@ -1,841 +0,0 @@ -/*Global.js*/ -/* -* Description: WebSvr -* Lincense: MIT, GPL -* Author: Kris Zhang -- Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -- Underscore: Add underscore a utility-belt library for JavaScript - https://github.com/documentcloud/underscore -- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime -*/ - -//Underscore global object; -var _ = require("./lib/underscore"); - -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); -*/ -/*Settings.js*/ -/* -Configurations -*/ -var Settings = { - version: 0.022, - - //root folder of web - root: "../", - - //list files in directory - listDir: false, - - //http - http: true, - //default port of http - port: 8054, - - //enable debug information output? - debug: false, - - //https - https: false, - //default port of https - httpsPort: 8443, - httpsOpts: { - key: require("fs").readFileSync("cert/privatekey.pem"), - cert: require("fs").readFileSync("cert/certificate.pem") - }, - - //session file stored here, must be end with "/" - sessionDir: "../tmp/session/", - //tempary upload file stored here, must be end with "/" - uploadDir: "../tmp/upload/" -}; - -/*ref\Math.uuid.js*/ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); - -/*Mapper.js*/ -/* -Mapper: Used for Filter & Handler, -expression: required parameter -handler: required parameter -options: other optional parameters -*/ - -var Mapper = function(expression, handler, options) { - var self = this; - - self.expression = expression; - self.handler = handler; - - //Has other parameters? - self.extend(options); -}; - -Mapper.prototype = { - /* - Does this mapper matched this request? - */ - match: function(req) { - var self = this, - expression = self.expression; - - //No expression? It's a general filter mapper - if (!expression) return true; - - switch(expression.constructor) { - case String: return req.url.indexOf(expression) > -1; - case RegExp: return expression.test(req.url); - } - - return false; - }, - - /* - Add optional parameters on current mapper - i.e: - session: boolean - file: boolean - parse: boolean - */ - extend: function(options) { - for(key in options) { - this[key] = options[key] - } - } -}; -/*RequestParser.js*/ -/* -Request parser, parse the data in request body via -when parse complete, execute the callback, with response data; -*/ -var RequestParser; - -(function() { - - //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE); - - RequestParser = function(req, res, callback) { - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); - }; - -}()); -/*SessionParser.js*/ - -var SessionParser; - -//TODO: Need a child process of clear session -(function() { - - var fs = require("fs"); - - SessionParser = (function(req, res, callback) { - - var sessionDir = Settings.sessionDir; - - var self = { - //session id - sid : null, - //session stored object - obj : {} - }; - - //TODO - self.set = function(key, val, callback) { - - var sessionfile = sessionDir + self.sid; - - key && (self.obj[key] = val); - - fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { - if (err) { - console.log(err); - return; - } - - callback && callback(self); - }); - }; - - //TO DO - self.get = function(key) { - return self.obj[key]; - }; - - self.init = function() { - var sidKey = "_wsid", - sidVal, - cookie = req.headers.cookie || ""; - - //Get or Create sid - var idx = cookie.indexOf(sidKey + "="); - - //sid exist in the cookie, read it - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - - //sid doesn't exist, create it; - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - }; - self.sid = sidVal; - - //We only receive the cookie from Http headers - var sessionfile = sessionDir + self.sid; - - //here will be cause a bit of delay - fs.exists(sessionfile, function (exists) { - if (exists) { - fs.readFile( sessionfile, function (err, data) { - if (err) { - console.log(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - callback(self); - }); - }else{ - //session not exist create one - self.obj = {}; - self.set(null , null , callback); - } - }); - - }; - - self.init(); - - return self; - - }); - -}()); -/*Parser.js*/ -/* -Parser: Functions that Filter and Handler will be called -*/ -var Parser = function(req, res, mapper) { - - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - SessionParser(req, res, function(session) { - req.session = session; - handler(req, res); - }); - }else{ - handler(req, res); - } - }; - - /* - parse data in request, this should be done before parse session, - because session stored in file - */ - var parseRequest = function() { - //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { - //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data) { - req.body = data; - parseSession(); - }); - }else{ - parseSession(); - } - }; - - /* - parse file in request, this should be at the top of the list - */ - var parseFile = function() { - //Need to parse the file in request? - if (mapper.file && typeof req.body == "undefined") { - //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'), - form = new formidable.IncomingForm(); - - form.uploadDir = Settings.uploadDir; - - form.parse(req, function(err, fields, files) { - if (err) { - console.log(err); - return; - }; - - //attach the parameters and files - req.body = fields; - req.files = files; - - //in fact request will not be parsed again, because body is not undefined - parseRequest(); - }); - }else{ - parseRequest(); - }; - }; - - parseFile(); - -}; - -/*Filter.js*/ -/* -Http Filter: Execute all the rules that matched, -Filters will be always called before a handler. -*/ -var Filter = { - //filter list - filters: [], - - /* - filter: add a new filter - expression: string/regexp [optional] - handler: function [required] - options: object [optional] - */ - filter: function(expression, handler, options) { - //The first parameter is Function => (handler, options) - if (expression.constructor == Function) { - options = handler; - handler = expression; - expression = null; - } - - var mapper = new Mapper(expression, handler, options); - Filter.filters.push(mapper); - }, - - /* - file receiver: it's a specfic filter, - this filter should be always at the top of the filter list - */ - file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); - //insert as the first elements - Filter.filters.splice(0, 0, mapper); - } -}; - -/* -Filter Chain -*/ -var FilterChain = function(cb) { - var self = this; - self.idx = 0; - self.cb = cb; -}; - -FilterChain.prototype = { - next: function(req, res) { - var self = this; - - var mapper = Filter.filters[self.idx++]; - - //filter is complete, execute callback; - if (!mapper) return self.cb && self.cb(); - - /* - If not Matched go to next filter - If matched need to execute the req.next() in callback handler, - e.g: - webSvr.filter(/expression/, function(req, res) { - //filter actions - req.next(req, res); - }, options); - */ - if (mapper.match(req)) { - - console.log("filter matched", self.idx, mapper.expression, req.url); - - Parser(req, res, mapper); - }else{ - self.next(req, res); - } - } -}; -/*Handler.js*/ -/* -Http Handler: Execute and returned when when first matched; -At the same time only one Handler will be called; -*/ -var Handler; - -(function() { - - /* - Private: handler list - */ - var handlers = []; - - /* - Static Handler instance - */ - Handler = { - /* - url: add a new handler - expression: string/regexp [required] - handler: [many types] [required] - options: object [optional] - */ - url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - handlers.push(mapper); - }, - - //Post: Parse the post data by default; - post: function(expression, handler, options) { - this.url(expression, handler, _.extend({ parse: true }, options)); - }, - - //Session: Parse the session and post by default; - session: function(expression, handler) { - this.url(expression, handler, { parse: true, session: true }); - }, - - handle: function(req, res) { - //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++) { - - var mapper = handlers[i]; - if (mapper.match(req)) { - - console.log("handler matched", i, mapper.expression, req.url); - - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type) { - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - return true; - } - } - - return false; - - } //end of handle - - }; - -}()); - - - - -/*ListDir.js*/ -/* -ListDir: List all the files in a directory -*/ -var ListDir = (function() { - - var fs = require("fs"), - path = require("path"); - - var urlFormat = function(url) { - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date) { - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num) { - return num + " ".substring(0, 12 - String(num).length); - }; - - //Create an anchor - var anchor = function(txt, url) { - url = url ? url : "/"; - return '' + txt + ""; - }; - - var listDir = { - //List all the files in a directory - list: function(req, res, dir) { - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function() { - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function() {
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files) { - if (err) { - listEnd(); - console.log(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++) { - //Persistent the idx before make the sync process - (function(idx) { - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat) { - cur++; - - if (err) { - console.log(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - } - }; - - return listDir; - -}()); -/*Server.js*/ -/* -* Description: Create a Web Server -* Author: Kris Zhang -* Licenses: MIT, GPL -*/ -/* -* Define WebSvr -* Export WebSvr -*/ -var WebSvr = module.exports = (function() { - - var server = function(options) { - //Library - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); - - //Parameters - //Count: How many files? - var self = this, - root, - port; - - var fileHandler = function(req, res) { - - var url = req.url, - hasQuery = url.indexOf("?"); - - //fs.stat can't recognize the file name with querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(root, url); - - fs.stat(fullPath, function(err, stat) { - - //Consider as file not found - if (err) return self.write404(res); - - //Is file? Open this file and send to client. - if (stat.isFile()) { - writeFile(res, fullPath); - } - - //Is Directory? List all the files and folders. - else if (stat.isDirectory()) { - options.listDir - ? ListDir.list(req, res, fullPath) - : self.write403(res); - } - - }); - }; - - var requestHandler = function(req, res) { - //Response may be shutdown when do the filter, in order not to cause exception, - //Rewrite the write/writeHead functionalities of current response object - var endFn = res.end; - res.end = function() { - //Execute old end - endFn.apply(res, arguments); - //Rewirte write/writeHead on response object - res.write = res.writeHead = function() { - console.log("response is already end, response.write ignored!") - }; - }; - - res.writeFile = function(filePath, cb) { - self.writeFile(res, filePath, cb); - }; - - res.redirect = function(url, status) { - res.writeHead(status ? status : 302, { "Location": url }); - res.end(); - }; - - //Define filter object - req.filter = new FilterChain(function() { - //if handler not match, send the request - !Handler.handle(req, res) && fileHandler(req, res); - }); - - //Handle the first filter - req.filter.next(req, res); - }; - - var writeFile = function(res, fullPath) { - fs.readFile(fullPath, function(err, data) { - if (err) { - console.log(err); - return; - } - res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); - res.end(data, "binary"); - }); - }; - - //Explose API - //Filter - self.filter = Filter.filter; - self.file = Filter.file; - - //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; - - //Get a fullpath of a request - self.getFullPath = function(filePath) { - return path.join(root, filePath); - }; - - //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb) { - filePath = path.join(root, filePath); - fs.exists(filePath, function(exist) { - if (exist) { - writeFile(res, filePath); - cb && cb(exist); - }else{ - //If callback function doesn't exist, write 404 page; - cb ? cb(exist) : self.write404(res); - } - }); - }; - - //TODO: Support 301 move permanently - - //TODO: Support 304 client-side cache - - self.write403 = function(res) { - res.writeHead(403, {"Content-Type": "text/html"}); - res.end("Access forbidden!"); - }; - - self.write404 = function(res) { - res.writeHead(404, {"Content-Type": "text/html"}); - res.end("File not found!"); - }; - - //Public: start http server - self.start = function() { - //Update the default value of Settings - options = _.extend({}, Settings, options); - - root = options.root; - port = parseInt(options.port); - - //Create http server - if (options.http) { - var httpSvr = require("http").createServer(requestHandler); - httpSvr.listen(port); - - console.log("Http server running at" - ,"Root:", root - ,"Port:", port - ); - - self.httpSvr = httpSvr; - } - - //Create https server - if (options.https) { - var httpsOpts = options.httpsOpts, - httpsPort = options.httpsPort; - - var httpsSvr = require("https").createServer(httpsOpts, requestHandler); - httpsSvr.listen(httpsPort); - - console.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); - - self.httpsSvr = httpsSvr; - } - - //diable console.log information - if (!options.debug) { - console.log = function(){}; - } - }; - - //Public: close http server; - self.close = function() { - self.httpSvr && self.httpSvr.close(); - self.httpsSvr && self.httpsSvr.close(); - }; - - }; - - return server; - -})(); diff --git a/test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL b/test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tmp/session/readme.txt b/tmp/session/readme.txt deleted file mode 100644 index 0b4b98a..0000000 --- a/tmp/session/readme.txt +++ /dev/null @@ -1 +0,0 @@ -Temporary session here. \ No newline at end of file diff --git a/tmp/upload/readme.txt b/tmp/upload/readme.txt deleted file mode 100644 index 9cf689f..0000000 --- a/tmp/upload/readme.txt +++ /dev/null @@ -1 +0,0 @@ -Temporary upload files here. \ No newline at end of file From 30ebe1f1f376bcf447eca39c3c03812413191b2c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 8 Nov 2012 11:43:12 +0800 Subject: [PATCH 028/195] Update testing codes. Update testing codes according new folder structure. --- test/index.htm | 31 ------------------------------- test/setting.htm | 17 ----------------- test/svr/sitetest.js | 9 +++++---- 3 files changed, 5 insertions(+), 52 deletions(-) delete mode 100644 test/index.htm delete mode 100644 test/setting.htm diff --git a/test/index.htm b/test/index.htm deleted file mode 100644 index 6cfc301..0000000 --- a/test/index.htm +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - Login - - - -

    WebSvr configuration login:

    -
    -
      -
    • - - - admin -
    • -
    • - - - 12345678 -
    • -
    • - - -
    • -
    -
    - - - diff --git a/test/setting.htm b/test/setting.htm deleted file mode 100644 index 39ed103..0000000 --- a/test/setting.htm +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Login - - -
    -
    - - -
    -
    - - - diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 9afc04f..f784f45 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -42,7 +42,7 @@ webSvr.filter(function(req, res) { }, {parse:true, session:true}); /* -Session Filter: protect test/* folder => (validation by session); +Session Filter: protect web/* folder => (validation by session); */ webSvr.filter(/web\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation @@ -69,9 +69,10 @@ webSvr.session("login.do", function(req, res) { //Put key/value pair in session //TODO: Support put JSON object directly req.session.set("username", qs.username, function(session) { - //TODO: Add req.redirect / req.forward functionalities; - res.writeHead(200, {"Content-Type": "text/html"}); - res.writeFile("/test/setting.htm"); + //res.writeHead(200, {"Content-Type": "text/html"}); + //res.writeFile("/web/setting.htm"); + //TODO: Error handler of undefined methods + res.redirect("/web/setting.htm"); }); }else{ res.writeHead(401); From ed49e1b09a155f2395a859df5b809b25216cea55 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 8 Nov 2012 11:43:45 +0800 Subject: [PATCH 029/195] remove tmp file remove tmp session file. --- test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL diff --git a/test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL b/test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL deleted file mode 100644 index 9e26dfe..0000000 --- a/test/tmp/session/gvZeW6ntTyyNcH2ZWm8fEFa7J7ORJFXL +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file From ca9b594fbc1c85e3627b4695cd079e2ea833eebf Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 8 Nov 2012 16:30:02 +0800 Subject: [PATCH 030/195] Template support Add template render support --- README.md | 8 +-- test/start.bat | 7 ++- test/svr/sitetest.js | 12 +++- test/web/template.node | 18 ++++++ websvr/Global.js | 10 ++- websvr/MakeFile.list | 1 + websvr/Server.js | 3 + websvr/SessionParser.js | 4 +- websvr/Template.js | 50 +++++++++++++++ websvr/build.bat | 2 +- websvr/lib/doT.js | 131 ++++++++++++++++++++++++++++++++++++++++ websvr/websvr.js | 68 +++++++++++++++++++-- 12 files changed, 298 insertions(+), 16 deletions(-) create mode 100644 test/web/template.node create mode 100644 websvr/Template.js create mode 100644 websvr/lib/doT.js diff --git a/README.md b/README.md index 22ac00b..1d8fde6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,10 @@ WebSvr ============== -Lincense: MIT, GPL - -Description: --------------- A simple web server based on node.js +Lincenses: MIT, GPL +Version: 0.0.36 -Version: 0.024 +Features -------------- - Filter: A request will try to match all the filters first, and then pass to the Handler - Handler: When a request matched a handler, it will returned, only one handler will be executed diff --git a/test/start.bat b/test/start.bat index 45982a7..89a808b 100644 --- a/test/start.bat +++ b/test/start.bat @@ -1,4 +1,9 @@ REM start sitetest + +:loop node svr/sitetest.js -pause; \ No newline at end of file +REM waiting... +pause; + +goto loop; \ No newline at end of file diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index f784f45..103ea02 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -98,6 +98,16 @@ webSvr.url("redirect", function(req, res) { res.redirect("/svr/websvr.all.js"); }); +/* +Template: render template with +*/ +webSvr.url("template.node", function(req, res) { + webSvr.render([req.url], { username: req.session.get("username") }, function(html){ + res.writeHead(200, {"Content-Type": "text/html"}); + res.end(html); + }); +}); + /* Simple redirect API: @@ -109,7 +119,7 @@ webSvr.url("hello", "Hello WebSvr!"); //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm webSvr.post("post.htm", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); - //Witch session support: "{session: true}" + //With session support: "{session: true}" res.write("You username is " + req.session.get("username")); res.write('

    '); res.end('Received : ' + req.body); diff --git a/test/web/template.node b/test/web/template.node new file mode 100644 index 0000000..61f1cd3 --- /dev/null +++ b/test/web/template.node @@ -0,0 +1,18 @@ + + + + + + Login + + + Hello: {{=it.username}} +
    +
    + + +
    +
    + + + diff --git a/websvr/Global.js b/websvr/Global.js index 77499b3..05fb1c1 100644 --- a/websvr/Global.js +++ b/websvr/Global.js @@ -1,12 +1,18 @@ /* * Description: WebSvr -* Lincense: MIT, GPL * Author: Kris Zhang +* Lincense: MIT, GPL +* Included Projects: +- Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ - Formidable: Support uploading files, integrate https://github.com/felixge/node-formidable/ - Underscore: Add underscore a utility-belt library for JavaScript https://github.com/documentcloud/underscore -- MIME: Suppor mime header, integrate https://github.com/broofa/node-mime +- MIME: content-type in header + https://github.com/broofa/node-mime +- template: Template Engine + https://github.com/olado/doT */ //Underscore global object; diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list index e22fdd7..c588499 100644 --- a/websvr/MakeFile.list +++ b/websvr/MakeFile.list @@ -17,4 +17,5 @@ Parser.js Filter.js Handler.js ListDir.js +Template.js Server.js \ No newline at end of file diff --git a/websvr/Server.js b/websvr/Server.js index fb8f343..18e5165 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -104,6 +104,9 @@ var WebSvr = module.exports = (function() { self.post = Handler.post; self.session = Handler.session; + //Template + self.render = Template.render; + //Get a fullpath of a request self.getFullPath = function(filePath) { return path.join(root, filePath); diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index 4ece4e6..78c081e 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -2,6 +2,7 @@ var SessionParser; //TODO: Need a child process of clear session +//TODO: Create session file when put sth. in (function() { var fs = require("fs"); @@ -17,7 +18,6 @@ var SessionParser; obj : {} }; - //TODO self.set = function(key, val, callback) { var sessionfile = sessionDir + self.sid; @@ -34,7 +34,7 @@ var SessionParser; }); }; - //TO DO + //TODO support async mode, add one parameter of callback self.get = function(key) { return self.obj[key]; }; diff --git a/websvr/Template.js b/websvr/Template.js new file mode 100644 index 0000000..468412d --- /dev/null +++ b/websvr/Template.js @@ -0,0 +1,50 @@ +/* +* Templates +*/ +var Template = (function() { + + var engine = require("./lib/doT"), + fs = require("fs"), + path = require("path"); + + //render a file + var renderFile = function(filename, cb){ + var fullpath = path.join(Settings.root, filename); + + fs.readFile(fullpath, function (err, html) { + err && console.log(err); + err ? cb("") : cb(html); + }); + }; + + return { + //render templates + render: function(chrunk, params, cb) { + var tmplFn = function(){}; + + //Not defined? passing an empty function + cb = cb || function(){}; + + try { + switch (chrunk.constructor) { + //It's html files + case String: + tmplFn = engine.compile(chrunk, params); + cb(tmplFn(params)); + break; + + //It's a file + case Array: + renderFile(chrunk[0], function(html) { + tmplFn = engine.compile(html, params); + cb(tmplFn(params)); + }); + break; + } + } catch (e) { + cb(e); + } + } + } + +}()); \ No newline at end of file diff --git a/websvr/build.bat b/websvr/build.bat index 264ea42..37566ea 100644 --- a/websvr/build.bat +++ b/websvr/build.bat @@ -1,5 +1,5 @@ REM Build websvr JS, and keep watching the changes -node tool/Combine.js -i makefile.list -o websvr.js +node tool/Combine.js -i makefile.list -o websvr.js -w REM Combine complete, Goodbye. pause; \ No newline at end of file diff --git a/websvr/lib/doT.js b/websvr/lib/doT.js new file mode 100644 index 0000000..c44f245 --- /dev/null +++ b/websvr/lib/doT.js @@ -0,0 +1,131 @@ +// doT.js +// 2011, Laura Doktorova, https://github.com/olado/doT +// +// doT.js is an open source component of http://bebedo.com +// Licensed under the MIT license. +// +(function() { + "use strict"; + + var doT = { + version: '0.2.0', + templateSettings: { + evaluate: /\{\{([\s\S]+?)\}\}/g, + interpolate: /\{\{=([\s\S]+?)\}\}/g, + encode: /\{\{!([\s\S]+?)\}\}/g, + use: /\{\{#([\s\S]+?)\}\}/g, + define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g, + conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g, + iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g, + varname: 'it', + strip: true, + append: true, + selfcontained: false + }, + template: undefined, //fn, compile template + compile: undefined //fn, for express + }; + + var global = (function(){ return this || (0,eval)('this'); }()); + + if (typeof module !== 'undefined' && module.exports) { + module.exports = doT; + } else if (typeof define === 'function' && define.amd) { + define(function(){return doT;}); + } else { + global.doT = doT; + } + + function encodeHTMLSource() { + var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }, + matchHTML = /&(?!#?\w+;)|<|>|"|'|\//g; + return function(code) { + return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : code; + }; + } + global.encodeHTML = encodeHTMLSource(); + + var startend = { + append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" }, + split: { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML("} + }, skip = /$^/; + + function resolveDefs(c, block, def) { + return ((typeof block === 'string') ? block : block.toString()) + .replace(c.define || skip, function(m, code, assign, value) { + if (code.indexOf('def.') === 0) { + code = code.substring(4); + } + if (!(code in def)) { + if (assign === ':') { + def[code]= value; + } else { + eval("def['"+code+"']=" + value); + } + } + return ''; + }) + .replace(c.use || skip, function(m, code) { + var v = eval(code); + return v ? resolveDefs(c, v, def) : v; + }); + } + + function unescape(code) { + return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, ' '); + } + + doT.template = function(tmpl, c, def) { + c = c || doT.templateSettings; + var cse = c.append ? startend.append : startend.split, str, needhtmlencode, sid=0, indv; + + if (c.use || c.define) { + var olddef = global.def; global.def = def || {}; // workaround minifiers + str = resolveDefs(c, tmpl, global.def); + global.def = olddef; + } else str = tmpl; + + str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g,' ') + .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,''): str) + .replace(/'|\\/g, '\\$&') + .replace(c.interpolate || skip, function(m, code) { + return cse.start + unescape(code) + cse.end; + }) + .replace(c.encode || skip, function(m, code) { + needhtmlencode = true; + return cse.startencode + unescape(code) + cse.end; + }) + .replace(c.conditional || skip, function(m, elsecase, code) { + return elsecase ? + (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") : + (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='"); + }) + .replace(c.iterate || skip, function(m, iterate, vname, iname) { + if (!iterate) return "';} } out+='"; + sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate); + return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+" Date: Fri, 9 Nov 2012 10:58:26 +0800 Subject: [PATCH 031/195] Filter: Update Get rid of the unused parameters --- test/svr/sitetest.js | 9 +++++++-- websvr/Filter.js | 19 +++++++++++++------ websvr/Server.js | 12 ++++++++---- websvr/websvr.js | 31 +++++++++++++++++++++---------- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 103ea02..353c2f8 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -38,7 +38,7 @@ webSvr.filter(function(req, res) { //res.write("Hello WebSvr!
    "); //Link to next filter - req.filter.next(req, res); + req.filter.next(); }, {parse:true, session:true}); /* @@ -51,7 +51,7 @@ webSvr.filter(/web\/[\w\.]+/, function(req, res) { } //Link to next filter - req.filter.next(req, res); + req.filter.next(); }); @@ -109,6 +109,11 @@ webSvr.url("template.node", function(req, res) { }); +webSvr.url("testres", function(req, res) { + res.test(); +}); + + /* Simple redirect API: */ diff --git a/websvr/Filter.js b/websvr/Filter.js index 3497163..81edf2e 100644 --- a/websvr/Filter.js +++ b/websvr/Filter.js @@ -30,7 +30,7 @@ var Filter = { */ file: function(expression, handler, options) { var mapper = new Mapper(expression, handler, {file: true}); - //insert as the first elements + //insert at the top of the filter array Filter.filters.splice(0, 0, mapper); } }; @@ -38,15 +38,21 @@ var Filter = { /* Filter Chain */ -var FilterChain = function(cb) { +var FilterChain = function(cb, req, res) { var self = this; + self.idx = 0; self.cb = cb; + + self.req = req; + self.res = res; }; FilterChain.prototype = { - next: function(req, res) { - var self = this; + next: function() { + var self = this, + req = self.req, + res = self.res; var mapper = Filter.filters[self.idx++]; @@ -63,12 +69,13 @@ FilterChain.prototype = { }, options); */ if (mapper.match(req)) { - console.log("filter matched", self.idx, mapper.expression, req.url); + //filter matched, parse the request and then execute it Parser(req, res, mapper); }else{ - self.next(req, res); + //filter not matched, validate next filter + self.next(); } } }; \ No newline at end of file diff --git a/websvr/Server.js b/websvr/Server.js index 18e5165..52b6b4e 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -73,14 +73,18 @@ var WebSvr = module.exports = (function() { res.end(); }; - //Define filter object - req.filter = new FilterChain(function() { + var filterChain = new FilterChain(function(){ + //if handler not match, send the request !Handler.handle(req, res) && fileHandler(req, res); - }); + + }, req, res); + + //Hook FilterChain object on the request + req.filter = filterChain; //Handle the first filter - req.filter.next(req, res); + req.filter.next(); }; var writeFile = function(res, fullPath) { diff --git a/websvr/websvr.js b/websvr/websvr.js index 44b3013..bf09738 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -425,7 +425,7 @@ var Filter = { */ file: function(expression, handler, options) { var mapper = new Mapper(expression, handler, {file: true}); - //insert as the first elements + //insert at the top of the filter array Filter.filters.splice(0, 0, mapper); } }; @@ -433,15 +433,21 @@ var Filter = { /* Filter Chain */ -var FilterChain = function(cb) { +var FilterChain = function(cb, req, res) { var self = this; + self.idx = 0; self.cb = cb; + + self.req = req; + self.res = res; }; FilterChain.prototype = { - next: function(req, res) { - var self = this; + next: function() { + var self = this, + req = self.req, + res = self.res; var mapper = Filter.filters[self.idx++]; @@ -458,12 +464,13 @@ FilterChain.prototype = { }, options); */ if (mapper.match(req)) { - console.log("filter matched", self.idx, mapper.expression, req.url); + //filter matched, parse the request and then execute it Parser(req, res, mapper); }else{ - self.next(req, res); + //filter not matched, validate next filter + self.next(); } } }; @@ -775,14 +782,18 @@ var WebSvr = module.exports = (function() { res.end(); }; - //Define filter object - req.filter = new FilterChain(function() { + var filterChain = new FilterChain(function(){ + //if handler not match, send the request !Handler.handle(req, res) && fileHandler(req, res); - }); + + }, req, res); + + //Hook FilterChain object on the request + req.filter = filterChain; //Handle the first filter - req.filter.next(req, res); + req.filter.next(); }; var writeFile = function(res, fullPath) { From 9c64d4fe1c772675c050766a27c308a88ea68a6c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 9 Nov 2012 17:03:56 +0800 Subject: [PATCH 032/195] Template: Make the interface more easier. Get rid of the unused parameters. --- test/svr/sitetest.js | 13 +---- test/web/template.node | 2 +- websvr/Logger.js | 36 ++++++++++++ websvr/MakeFile.list | 1 + websvr/Parser.js | 2 +- websvr/Server.js | 8 ++- websvr/Settings.js | 8 ++- websvr/Template.js | 66 +++++++++++++--------- websvr/websvr.js | 121 ++++++++++++++++++++++++++++++----------- 9 files changed, 184 insertions(+), 73 deletions(-) create mode 100644 websvr/Logger.js diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 353c2f8..22f3cd1 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -102,18 +102,11 @@ webSvr.url("redirect", function(req, res) { Template: render template with */ webSvr.url("template.node", function(req, res) { - webSvr.render([req.url], { username: req.session.get("username") }, function(html){ - res.writeHead(200, {"Content-Type": "text/html"}); - res.end(html); - }); -}); - - -webSvr.url("testres", function(req, res) { - res.test(); + res.writeHead(200, {"Content-Type": "text/html"}); + //render request with session username; + res.render(req, {username: req.session.get("username")}); }); - /* Simple redirect API: */ diff --git a/test/web/template.node b/test/web/template.node index 61f1cd3..ac6d097 100644 --- a/test/web/template.node +++ b/test/web/template.node @@ -6,7 +6,7 @@ Login - Hello: {{=it.username}} + Hello: {{=lt.username}}
    diff --git a/websvr/Logger.js b/websvr/Logger.js new file mode 100644 index 0000000..75a5042 --- /dev/null +++ b/websvr/Logger.js @@ -0,0 +1,36 @@ +/* +Logger: log sth +*/ +var Logger = (function(){ + + var fs = require("fs"), + path = require("path"); + + var lineSeparator = "\r\n", + indentSeparator = "\t", + depth = 9; + + var log = function(logObj){ + + var output = new Date() + lineSeparator; + + function print(pre, obj){ + if(!obj) return; + for(var key in obj){ + output = output + pre + key + " : " + obj[key] + lineSeparator; + if(typeof obj[key] == "object"){ + (pre.length < depth) && print(pre + indentSeparator, obj[key]); + } + } + } + + print(indentSeparator, logObj); + + fs.appendFile(Settings.logger, output, function(err){ + console.log(err); + }); + }; + + return { log: log }; + +})(); \ No newline at end of file diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list index c588499..d52004c 100644 --- a/websvr/MakeFile.list +++ b/websvr/MakeFile.list @@ -5,6 +5,7 @@ #Global object and description Global.js Settings.js +Logger.js #Reference: JavaScript library ref/Math.uuid.js diff --git a/websvr/Parser.js b/websvr/Parser.js index 33e8753..ed57a2b 100644 --- a/websvr/Parser.js +++ b/websvr/Parser.js @@ -54,7 +54,7 @@ var Parser = function(req, res, mapper) { }; //attach the parameters and files - req.body = fields; + req.body = fields; req.files = files; //in fact request will not be parsed again, because body is not undefined diff --git a/websvr/Server.js b/websvr/Server.js index 52b6b4e..8e47d3e 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -73,6 +73,10 @@ var WebSvr = module.exports = (function() { res.end(); }; + //render template objects + res.render = Template.render; + + //initial httprequest var filterChain = new FilterChain(function(){ //if handler not match, send the request @@ -108,8 +112,8 @@ var WebSvr = module.exports = (function() { self.post = Handler.post; self.session = Handler.session; - //Template - self.render = Template.render; + //Logger + self.log = Logger.log; //Get a fullpath of a request self.getFullPath = function(filePath) { diff --git a/websvr/Settings.js b/websvr/Settings.js index 272cc0c..93b9e42 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -24,8 +24,12 @@ var Settings = { httpsPort: 8443, httpsOpts: { key:"", cert:"" }, + //logger file path + logger: "./tmp/log.txt", + //session file stored here, must be end with "/" - sessionDir: "../tmp/session/", + sessionDir: "./tmp/session/", + //tempary upload file stored here, must be end with "/" - uploadDir: "../tmp/upload/" + uploadDir: "./tmp/upload/" }; diff --git a/websvr/Template.js b/websvr/Template.js index 468412d..7d22197 100644 --- a/websvr/Template.js +++ b/websvr/Template.js @@ -7,8 +7,8 @@ var Template = (function() { fs = require("fs"), path = require("path"); - //render a file - var renderFile = function(filename, cb){ + //get a file + var getFile = function(filename, cb){ var fullpath = path.join(Settings.root, filename); fs.readFile(fullpath, function (err, html) { @@ -17,33 +17,47 @@ var Template = (function() { }); }; + //render a file + var render = function(chrunk, params, outFn){ + try { + tmplFn = engine.compile(chrunk, params); + outFn(tmplFn(params)); + } catch(err) { + console.log(err); + outFn(err); + } + }; + return { //render templates - render: function(chrunk, params, cb) { - var tmplFn = function(){}; - - //Not defined? passing an empty function - cb = cb || function(){}; - - try { - switch (chrunk.constructor) { - //It's html files - case String: - tmplFn = engine.compile(chrunk, params); - cb(tmplFn(params)); - break; - - //It's a file - case Array: - renderFile(chrunk[0], function(html) { - tmplFn = engine.compile(html, params); - cb(tmplFn(params)); - }); - break; - } - } catch (e) { - cb(e); + render: function(chrunk, params) { + var res = this, + end = res.end; + + var url = chrunk.url, + con = chrunk.constructor; + + //It's a http request (it has "url") + if (url) { + getFile(url, function(tmpl) { + render(tmpl, params, end); + }); + + //It's html contents (template codes) + } else if (con == String) { + render(chrunk, params, end); + + //It's Array object (template file path) + } else if (con == Array) { + getFile(chrunk[0], function(tmpl) { + render(tmpl, params, end); + }); + + //Nothing matched end the response + } else { + end(); } + } } diff --git a/websvr/websvr.js b/websvr/websvr.js index bf09738..933681a 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -50,12 +50,53 @@ var Settings = { httpsPort: 8443, httpsOpts: { key:"", cert:"" }, + //logger file path + logger: "./tmp/log.txt", + //session file stored here, must be end with "/" - sessionDir: "../tmp/session/", + sessionDir: "./tmp/session/", + //tempary upload file stored here, must be end with "/" - uploadDir: "../tmp/upload/" + uploadDir: "./tmp/upload/" }; +/*Logger.js*/ +/* +Logger: log sth +*/ +var Logger = (function(){ + + var fs = require("fs"), + path = require("path"); + + var lineSeparator = "\r\n", + indentSeparator = "\t", + depth = 9; + + var log = function(logObj){ + + var output = new Date() + lineSeparator; + + function print(pre, obj){ + if(!obj) return; + for(var key in obj){ + output = output + pre + key + " : " + obj[key] + lineSeparator; + if(typeof obj[key] == "object"){ + (pre.length < depth) && print(pre + indentSeparator, obj[key]); + } + } + } + + print(indentSeparator, logObj); + + fs.appendFile(Settings.logger, output, function(err){ + console.log(err); + }); + }; + + return { log: log }; + +})(); /*ref\Math.uuid.js*/ /*! Math.uuid.js (v1.4) @@ -377,7 +418,7 @@ var Parser = function(req, res, mapper) { }; //attach the parameters and files - req.body = fields; + req.body = fields; req.files = files; //in fact request will not be parsed again, because body is not undefined @@ -665,8 +706,8 @@ var Template = (function() { fs = require("fs"), path = require("path"); - //render a file - var renderFile = function(filename, cb){ + //get a file + var getFile = function(filename, cb){ var fullpath = path.join(Settings.root, filename); fs.readFile(fullpath, function (err, html) { @@ -675,33 +716,47 @@ var Template = (function() { }); }; + //render a file + var render = function(chrunk, params, outFn){ + try { + tmplFn = engine.compile(chrunk, params); + outFn(tmplFn(params)); + } catch(err) { + console.log(err); + outFn(err); + } + }; + return { //render templates - render: function(chrunk, params, cb) { - var tmplFn = function(){}; - - //Not defined? passing an empty function - cb = cb || function(){}; - - try { - switch (chrunk.constructor) { - //It's html files - case String: - tmplFn = engine.compile(chrunk, params); - cb(tmplFn(params)); - break; - - //It's a file - case Array: - renderFile(chrunk[0], function(html) { - tmplFn = engine.compile(html, params); - cb(tmplFn(params)); - }); - break; - } - } catch (e) { - cb(e); + render: function(chrunk, params) { + var res = this, + end = res.end; + + var url = chrunk.url, + con = chrunk.constructor; + + //It's a http request (it has "url") + if (url) { + getFile(url, function(tmpl) { + render(tmpl, params, end); + }); + + //It's html contents (template codes) + } else if (con == String) { + render(chrunk, params, end); + + //It's Array object (template file path) + } else if (con == Array) { + getFile(chrunk[0], function(tmpl) { + render(tmpl, params, end); + }); + + //Nothing matched end the response + } else { + end(); } + } } @@ -782,6 +837,10 @@ var WebSvr = module.exports = (function() { res.end(); }; + //render template objects + res.render = Template.render; + + //initial httprequest var filterChain = new FilterChain(function(){ //if handler not match, send the request @@ -817,8 +876,8 @@ var WebSvr = module.exports = (function() { self.post = Handler.post; self.session = Handler.session; - //Template - self.render = Template.render; + //Logger + self.log = Logger.log; //Get a fullpath of a request self.getFullPath = function(filePath) { From 2bb215797b7fa992911965c00da96d040d917200 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 10 Nov 2012 12:18:23 +0800 Subject: [PATCH 033/195] Library reference; Remove the duplicate reference --- test/web/template.node | 2 +- websvr/Global.js | 13 +++++++------ websvr/ListDir.js | 3 --- websvr/Logger.js | 3 --- websvr/Server.js | 4 ---- websvr/SessionParser.js | 2 -- websvr/Template.js | 4 +--- websvr/websvr.js | 31 +++++++++---------------------- 8 files changed, 18 insertions(+), 44 deletions(-) diff --git a/test/web/template.node b/test/web/template.node index ac6d097..61f1cd3 100644 --- a/test/web/template.node +++ b/test/web/template.node @@ -6,7 +6,7 @@ Login - Hello: {{=lt.username}} + Hello: {{=it.username}}
    diff --git a/websvr/Global.js b/websvr/Global.js index 05fb1c1..6fd0fb0 100644 --- a/websvr/Global.js +++ b/websvr/Global.js @@ -15,10 +15,11 @@ https://github.com/olado/doT */ -//Underscore global object; -var _ = require("./lib/underscore"); +//Node library +var fs = require("fs"); +var path = require("path"); +var qs = require("querystring"); -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); -*/ \ No newline at end of file +//Open source library +var _ = require("./lib/underscore"); +var mime = require("./lib/mime") \ No newline at end of file diff --git a/websvr/ListDir.js b/websvr/ListDir.js index 5d773f0..da788cd 100644 --- a/websvr/ListDir.js +++ b/websvr/ListDir.js @@ -3,9 +3,6 @@ ListDir: List all the files in a directory */ var ListDir = (function() { - var fs = require("fs"), - path = require("path"); - var urlFormat = function(url) { url = url.replace(/\\/g,'/'); url = url.replace(/ /g,'%20'); diff --git a/websvr/Logger.js b/websvr/Logger.js index 75a5042..86c8d95 100644 --- a/websvr/Logger.js +++ b/websvr/Logger.js @@ -3,9 +3,6 @@ Logger: log sth */ var Logger = (function(){ - var fs = require("fs"), - path = require("path"); - var lineSeparator = "\r\n", indentSeparator = "\t", depth = 9; diff --git a/websvr/Server.js b/websvr/Server.js index 8e47d3e..34e72a0 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -10,10 +10,6 @@ var WebSvr = module.exports = (function() { var server = function(options) { - //Library - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); //Parameters //Count: How many files? diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index 78c081e..f944e1c 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -5,8 +5,6 @@ var SessionParser; //TODO: Create session file when put sth. in (function() { - var fs = require("fs"); - SessionParser = (function(req, res, callback) { var sessionDir = Settings.sessionDir; diff --git a/websvr/Template.js b/websvr/Template.js index 7d22197..b662842 100644 --- a/websvr/Template.js +++ b/websvr/Template.js @@ -3,9 +3,7 @@ */ var Template = (function() { - var engine = require("./lib/doT"), - fs = require("fs"), - path = require("path"); + var engine = require("./lib/doT"); //get a file var getFile = function(filename, cb){ diff --git a/websvr/websvr.js b/websvr/websvr.js index 933681a..7d1655b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -16,13 +16,14 @@ https://github.com/olado/doT */ -//Underscore global object; -var _ = require("./lib/underscore"); - -//TODO: Should add global reference here? i.e. -/* -var fs = require("fs"); -*/ +//Node library +var fs = require("fs"); +var path = require("path"); +var qs = require("querystring"); + +//Open source library +var _ = require("./lib/underscore"); +var mime = require("./lib/mime") /*Settings.js*/ /* Configurations @@ -66,9 +67,6 @@ Logger: log sth */ var Logger = (function(){ - var fs = require("fs"), - path = require("path"); - var lineSeparator = "\r\n", indentSeparator = "\t", depth = 9; @@ -277,8 +275,6 @@ var SessionParser; //TODO: Create session file when put sth. in (function() { - var fs = require("fs"); - SessionParser = (function(req, res, callback) { var sessionDir = Settings.sessionDir; @@ -604,9 +600,6 @@ ListDir: List all the files in a directory */ var ListDir = (function() { - var fs = require("fs"), - path = require("path"); - var urlFormat = function(url) { url = url.replace(/\\/g,'/'); url = url.replace(/ /g,'%20'); @@ -702,9 +695,7 @@ var ListDir = (function() { */ var Template = (function() { - var engine = require("./lib/doT"), - fs = require("fs"), - path = require("path"); + var engine = require("./lib/doT"); //get a file var getFile = function(filename, cb){ @@ -774,10 +765,6 @@ var Template = (function() { var WebSvr = module.exports = (function() { var server = function(options) { - //Library - var fs = require("fs"), - path = require("path"), - mime = require("./lib/mime"); //Parameters //Count: How many files? From b6084b20c0667502fd183b4c9b962374a8837ab7 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Mon, 12 Nov 2012 17:36:50 +0800 Subject: [PATCH 034/195] Session update; Add the temp session file when set sth in. --- test/svr/sitetest.js | 11 ++- websvr/Parser.js | 8 +-- websvr/SessionParser.js | 146 ++++++++++++++++++++++--------------- websvr/websvr.js | 154 ++++++++++++++++++++++++---------------- 4 files changed, 193 insertions(+), 126 deletions(-) diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 22f3cd1..cafa32a 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -47,7 +47,11 @@ Session Filter: protect web/* folder => (validation by session); webSvr.filter(/web\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - !req.session.get("username") && res.end("You must login, first!"); + req.session.get("username", function(val){ + console.log("session", val); + + !val && res.end("You must login, first!"); + }); } //Link to next filter @@ -72,6 +76,7 @@ webSvr.session("login.do", function(req, res) { //res.writeHead(200, {"Content-Type": "text/html"}); //res.writeFile("/web/setting.htm"); //TODO: Error handler of undefined methods + console.log(session); res.redirect("/web/setting.htm"); }); }else{ @@ -104,7 +109,9 @@ Template: render template with webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //render request with session username; - res.render(req, {username: req.session.get("username")}); + req.session.get(function(session){ + res.render(req, {username: session["username"]}); + }); }); /* diff --git a/websvr/Parser.js b/websvr/Parser.js index ed57a2b..b9ffcdd 100644 --- a/websvr/Parser.js +++ b/websvr/Parser.js @@ -9,13 +9,9 @@ var Parser = function(req, res, mapper) { var parseSession = function() { //add sesion support if (mapper.session && typeof req.session == "undefined") { - SessionParser(req, res, function(session) { - req.session = session; - handler(req, res); - }); - }else{ - handler(req, res); + req.session = new SessionParser(req, res); } + handler(req, res); }; /* diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index f944e1c..387e3a1 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -1,67 +1,108 @@ - + var SessionParser; //TODO: Need a child process of clear session -//TODO: Create session file when put sth. in (function() { - SessionParser = (function(req, res, callback) { - - var sessionDir = Settings.sessionDir; - - var self = { - //session id - sid : null, - //session stored object - obj : {} - }; - - self.set = function(key, val, callback) { - - var sessionfile = sessionDir + self.sid; + SessionParser = function(req, res){ + var self = this; - key && (self.obj[key] = val); - - fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { - if (err) { - console.log(err); - return; - } - - callback && callback(self); - }); - }; + //session id + self.sid = null; + //session stored object + self.obj = null; + //this is new session? + self.new = false; - //TODO support async mode, add one parameter of callback - self.get = function(key) { - return self.obj[key]; - }; + //sessoin file path + self.path = null; - self.init = function() { - var sidKey = "_wsid", - sidVal, - cookie = req.headers.cookie || ""; + //init session object + self.init(req, res); + }; - //Get or Create sid - var idx = cookie.indexOf(sidKey + "="); + SessionParser.prototype = { + init: function(req, res) { + var self = this, + sidKey = "_wsid", + sidVal; - //sid exist in the cookie, read it + //Get or Create sid, sid exist in the cookie, read it + var cookie = req.headers.cookie || ""; + var idx = cookie.indexOf(sidKey + "="); (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - //sid doesn't exist, create it; + //Sid doesn't exist, create it if (idx < 0 || sidVal.length != 32) { sidVal = Math.uuid(32); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + self.new = true; }; self.sid = sidVal; - //We only receive the cookie from Http headers - var sessionfile = sessionDir + self.sid; + //Update sessionfile path + self.path = path.join(Settings.sessionDir, self.sid); + } + + //Create new session object + , newObj: function(key, cb){ + //Key is offered, return null of this key, else return empty session object + var val = key ? null : {}; + + this.obj = {}; + cb && cb(val); + return val; + } + + //Get value from session object + , getVal: function(key, cb){ + //key is null, return all the session object + var val = key ? this.obj[key] : this.obj; + cb && cb(val); + return val; + } + + //Set an key/value pair in session object + , set: function(key, val, cb) { + var self = this; + + //Get session object first + self.get(function() { + + //Add or update key/value in session object + self.obj[key] = val; + + fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } + + cb && cb(self.obj); + }); + }); + } + + //Get value from session file, callbak only need to be appllied once; + , get: function(key, cb) { + var self = this; + + //The first parameter is callback function + if (key.constructor == Function) { + cb = key; + key = null; + } + + //The session object is already loaded + if (self.obj) return self.getVal(key, cb); - //here will be cause a bit of delay - fs.exists(sessionfile, function (exists) { + //It's a new session file, need not to load it from file + if (self.new) return self.newObj(key, cb); + + //File operates, a bit of delay + fs.exists(self.path, function(exists) { if (exists) { - fs.readFile( sessionfile, function (err, data) { + fs.readFile(self.path, function(err, data) { if (err) { console.log(err); return; @@ -69,21 +110,14 @@ var SessionParser; data = data || "{}"; self.obj = JSON.parse(data); - callback(self); + return self.getVal(key, cb); }); - }else{ - //session not exist create one - self.obj = {}; - self.set(null , null , callback); + } else { + return self.newObj(key, cb); } }); + } - }; - - self.init(); - - return self; - - }); + }; }()); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 7d1655b..8617831 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -268,70 +268,111 @@ var RequestParser; }()); /*SessionParser.js*/ - + var SessionParser; //TODO: Need a child process of clear session -//TODO: Create session file when put sth. in (function() { - SessionParser = (function(req, res, callback) { - - var sessionDir = Settings.sessionDir; - - var self = { - //session id - sid : null, - //session stored object - obj : {} - }; - - self.set = function(key, val, callback) { + SessionParser = function(req, res){ + var self = this; - var sessionfile = sessionDir + self.sid; + //session id + self.sid = null; + //session stored object + self.obj = null; + //this is new session? + self.new = false; - key && (self.obj[key] = val); + //sessoin file path + self.path = null; - fs.writeFile(sessionfile, JSON.stringify(self.obj), function(err) { - if (err) { - console.log(err); - return; - } - - callback && callback(self); - }); - }; - - //TODO support async mode, add one parameter of callback - self.get = function(key) { - return self.obj[key]; - }; - - self.init = function() { - var sidKey = "_wsid", - sidVal, - cookie = req.headers.cookie || ""; + //init session object + self.init(req, res); + }; - //Get or Create sid - var idx = cookie.indexOf(sidKey + "="); + SessionParser.prototype = { + init: function(req, res) { + var self = this, + sidKey = "_wsid", + sidVal; - //sid exist in the cookie, read it + //Get or Create sid, sid exist in the cookie, read it + var cookie = req.headers.cookie || ""; + var idx = cookie.indexOf(sidKey + "="); (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - //sid doesn't exist, create it; + //Sid doesn't exist, create it if (idx < 0 || sidVal.length != 32) { sidVal = Math.uuid(32); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + self.new = true; }; self.sid = sidVal; - //We only receive the cookie from Http headers - var sessionfile = sessionDir + self.sid; + //Update sessionfile path + self.path = path.join(Settings.sessionDir, self.sid); + } + + //Create new session object + , newObj: function(key, cb){ + //Key is offered, return null of this key, else return empty session object + var val = key ? null : {}; + + this.obj = {}; + cb && cb(val); + return val; + } + + //Get value from session object + , getVal: function(key, cb){ + //key is null, return all the session object + var val = key ? this.obj[key] : this.obj; + cb && cb(val); + return val; + } + + //Set an key/value pair in session object + , set: function(key, val, cb) { + var self = this; + + //Get session object first + self.get(function() { + + //Add or update key/value in session object + self.obj[key] = val; + + fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } + + cb && cb(self.obj); + }); + }); + } + + //Get value from session file, callbak only need to be appllied once; + , get: function(key, cb) { + var self = this; + + //The first parameter is callback function + if (key.constructor == Function) { + cb = key; + key = null; + } - //here will be cause a bit of delay - fs.exists(sessionfile, function (exists) { + //The session object is already loaded + if (self.obj) return self.getVal(key, cb); + + //It's a new session file, need not to load it from file + if (self.new) return self.newObj(key, cb); + + //File operates, a bit of delay + fs.exists(self.path, function(exists) { if (exists) { - fs.readFile( sessionfile, function (err, data) { + fs.readFile(self.path, function(err, data) { if (err) { console.log(err); return; @@ -339,22 +380,15 @@ var SessionParser; data = data || "{}"; self.obj = JSON.parse(data); - callback(self); + return self.getVal(key, cb); }); - }else{ - //session not exist create one - self.obj = {}; - self.set(null , null , callback); + } else { + return self.newObj(key, cb); } }); + } - }; - - self.init(); - - return self; - - }); + }; }()); /*Parser.js*/ @@ -369,13 +403,9 @@ var Parser = function(req, res, mapper) { var parseSession = function() { //add sesion support if (mapper.session && typeof req.session == "undefined") { - SessionParser(req, res, function(session) { - req.session = session; - handler(req, res); - }); - }else{ - handler(req, res); + req.session = new SessionParser(req, res); } + handler(req, res); }; /* From a8a3ea785f0d93b9cd3247e0e17b17c8cf1d88b7 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 13 Nov 2012 10:08:53 +0800 Subject: [PATCH 035/195] README: update Update samples --- README.md | 71 ++++++++++++++++++++++++++++++++++---------- test/svr/sitetest.js | 2 +- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1d8fde6..bbeaa5e 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,29 @@ Start -------------- Edit in SiteTest.js or Create a new Site.js and added to MakeFile.list - //Start the WebSVr, runnting at parent folder, default port is 8054, directory browser enabled; + //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 var webSvr = new WebSvr({ - root: "../", - listDir: true, + root: "./", + + //enable https https: true, - httpsPort: 8443 + //default port of https + httpsPort: 8443, + httpsOpts: { + key: require("fs").readFileSync("svr/cert/privatekey.pem"), + cert: require("fs").readFileSync("svr/cert/certificate.pem") + }, + + //Change the default locations of tmp session and upload files + //session file stored here, must be end with "/" + sessionDir: "tmp/session/", + //tempary upload file stored here, must be end with "/" + uploadDir: "tmp/upload/", + + + listDir: true, + debug: true }); webSvr.start(); @@ -35,25 +51,29 @@ Session based authentication (session stored in files), all the request under "t parse: parse the post data and stored in req.body; session: init the session and stored in req.session; */ - webSvr.filter(function(req, res){ + webSvr.filter(function(req, res) { //TODO: Add greeting words in filter //res.write("Hello WebSvr!
    "); //Link to next filter - req.filter.next(req, res); + req.filter.next(); }, {parse:true, session:true}); /* - Session Filter: protect test/* folder => (validate by session); + Session Filter: protect web/* folder => (validation by session); */ - webSvr.filter(/test\/[\w\.]+/, function(req, res) { + webSvr.filter(/web\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - !req.session.get("username") && res.end("You must login, first!"); + req.session.get("username", function(val){ + console.log("session", val); + + !val && res.end("You must login, first!"); + }); } //Link to next filter - req.filter.next(req, res); + req.filter.next(); }); @@ -66,7 +86,7 @@ Handle Login and put the username in Session username: admin password: 12345678 */ - webSvr.session(/login.do/, function(req, res) { + webSvr.session("login.do", function(req, res) { var querystring = require("querystring"); //TODO: Add an parameter to auto-complete querystring.parse(req.body); @@ -75,9 +95,11 @@ Handle Login and put the username in Session //Put key/value pair in session //TODO: Support put JSON object directly req.session.set("username", qs.username, function(session) { - //TODO: Add req.redirect / req.forward functionalities; - res.writeHead(200, {"Content-Type": "text/html"}); - res.writeFile("/test/setting.htm"); + //res.writeHead(200, {"Content-Type": "text/html"}); + //res.writeFile("/web/setting.htm"); + //TODO: Error handler of undefined methods + console.log(session); + res.redirect("/web/setting.htm"); }); }else{ res.writeHead(401); @@ -92,7 +114,7 @@ Receive upload file (it's a specfic filter) /* Uploader: upload.do => (receive handler) */ - webSvr.file(/upload.do/, function(req, res) { + webSvr.file("upload.do", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); //Upload file is stored in req.files //form fields is stored in req.body @@ -100,6 +122,25 @@ Receive upload file (it's a specfic filter) res.end(JSON.stringify(req.files)); }); +Redirect +-------------- +Redirect request, try at: http://localhost:8054/redirect + + webSvr.url("redirect", function(req, res) { + res.redirect("/svr/websvr.all.js"); + }); + +Template: +-------------- +Render template with params + + webSvr.url("template.node", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //render request with session username; + req.session.get(function(session){ + res.render(req, {username: session["username"]}); + }); + }); Other APIs -------------- diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index cafa32a..1e61067 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -104,7 +104,7 @@ webSvr.url("redirect", function(req, res) { }); /* -Template: render template with +Template: render template with params */ webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); From a4d9523d6bec6d772f83aa9e2ec4e86be37566d0 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 13 Nov 2012 10:16:01 +0800 Subject: [PATCH 036/195] Format codes Format codes --- README.md | 2 +- test/svr/sitetest.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bbeaa5e..fbdce74 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Redirect request, try at: http://localhost:8054/redirect res.redirect("/svr/websvr.all.js"); }); -Template: +Template -------------- Render template with params diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 1e61067..ccc41b5 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -47,7 +47,7 @@ Session Filter: protect web/* folder => (validation by session); webSvr.filter(/web\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - req.session.get("username", function(val){ + req.session.get("username", function(val) { console.log("session", val); !val && res.end("You must login, first!"); @@ -79,7 +79,7 @@ webSvr.session("login.do", function(req, res) { console.log(session); res.redirect("/web/setting.htm"); }); - }else{ + } else { res.writeHead(401); res.end("Wrong username/password"); } @@ -109,7 +109,7 @@ Template: render template with params webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //render request with session username; - req.session.get(function(session){ + req.session.get(function(session) { res.render(req, {username: session["username"]}); }); }); From a099b6ca40958ddb8a08d61e0d76f290c9be4b89 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 14 Nov 2012 14:27:18 +0800 Subject: [PATCH 037/195] Format & Rename Format: Remove unused closure; Rename: Rename RequestParser to BodyParser --- websvr/BodyParser.js | 20 ++++ websvr/MakeFile.list | 2 +- websvr/Mapper.js | 6 +- websvr/Parser.js | 8 +- websvr/RequestParser.js | 27 ----- websvr/Server.js | 3 +- websvr/SessionParser.js | 208 ++++++++++++++++---------------- websvr/Settings.js | 4 +- websvr/websvr.js | 254 +++++++++++++++++++--------------------- 9 files changed, 256 insertions(+), 276 deletions(-) create mode 100644 websvr/BodyParser.js delete mode 100644 websvr/RequestParser.js diff --git a/websvr/BodyParser.js b/websvr/BodyParser.js new file mode 100644 index 0000000..cb2ea54 --- /dev/null +++ b/websvr/BodyParser.js @@ -0,0 +1,20 @@ +/* +Body parser, parse the data in request body via +when parse complete, execute the callback, with response data; +*/ +var BodyParser = function(req, res, callback) { + + var buffer = new Buffer(Settings.bufferSize); + + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); +}; \ No newline at end of file diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list index d52004c..b0b6bf8 100644 --- a/websvr/MakeFile.list +++ b/websvr/MakeFile.list @@ -12,7 +12,7 @@ ref/Math.uuid.js #WebSvr Modules Mapper.js -RequestParser.js +BodyParser.js SessionParser.js Parser.js Filter.js diff --git a/websvr/Mapper.js b/websvr/Mapper.js index 2f31181..4af881c 100644 --- a/websvr/Mapper.js +++ b/websvr/Mapper.js @@ -1,8 +1,8 @@ /* Mapper: Used for Filter & Handler, expression: required parameter -handler: required parameter -options: other optional parameters +handler: required parameter +options: optional parameters */ var Mapper = function(expression, handler, options) { @@ -26,7 +26,7 @@ Mapper.prototype = { //No expression? It's a general filter mapper if (!expression) return true; - switch(expression.constructor) { + switch (expression.constructor) { case String: return req.url.indexOf(expression) > -1; case RegExp: return expression.test(req.url); } diff --git a/websvr/Parser.js b/websvr/Parser.js index b9ffcdd..361e8ec 100644 --- a/websvr/Parser.js +++ b/websvr/Parser.js @@ -18,11 +18,11 @@ var Parser = function(req, res, mapper) { parse data in request, this should be done before parse session, because session stored in file */ - var parseRequest = function() { + var parseBody = function() { //need to parse the request? if (mapper.parse && typeof req.body == "undefined") { //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data) { + BodyParser(req, res, function(data) { req.body = data; parseSession(); }); @@ -54,10 +54,10 @@ var Parser = function(req, res, mapper) { req.files = files; //in fact request will not be parsed again, because body is not undefined - parseRequest(); + parseBody(); }); }else{ - parseRequest(); + parseBody(); }; }; diff --git a/websvr/RequestParser.js b/websvr/RequestParser.js deleted file mode 100644 index 2794277..0000000 --- a/websvr/RequestParser.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -Request parser, parse the data in request body via -when parse complete, execute the callback, with response data; -*/ -var RequestParser; - -(function() { - - //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE); - - RequestParser = function(req, res, callback) { - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); - }; - -}()); \ No newline at end of file diff --git a/websvr/Server.js b/websvr/Server.js index 34e72a0..53a71b4 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -4,8 +4,7 @@ * Licenses: MIT, GPL */ /* -* Define WebSvr -* Export WebSvr +* Define and Export WebSvr */ var WebSvr = module.exports = (function() { diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index 387e3a1..40e4c4f 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -1,123 +1,119 @@ - -var SessionParser; - +/* +Parse request with session support +*/ //TODO: Need a child process of clear session -(function() { - - SessionParser = function(req, res){ +var SessionParser = function(req, res){ + var self = this; + + //session id + self.sid = null; + //session stored object + self.obj = null; + //is this new session? + self.new = false; + + //sessoin file path + self.path = null; + + //init session object + self.init(req, res); +}; + +SessionParser.prototype = { + init: function(req, res) { + var self = this, + sidKey = "_wsid", + sidVal; + + //Get or Create sid, sid exist in the cookie, read it + var cookie = req.headers.cookie || ""; + var idx = cookie.indexOf(sidKey + "="); + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //Sid doesn't exist, create it + if (idx < 0 || sidVal.length != 32) { + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + self.new = true; + }; + self.sid = sidVal; + + //Update sessionfile path + self.path = path.join(Settings.sessionDir, self.sid); + } + + //Create new session object + , newObj: function(key, cb){ + //Key is offered, return null of this key, else return empty session object + var val = key ? null : {}; + + this.obj = {}; + cb && cb(val); + return val; + } + + //Get value from session object + , getVal: function(key, cb){ + //key is null, return all the session object + var val = key ? this.obj[key] : this.obj; + cb && cb(val); + return val; + } + + //Set an key/value pair in session object + , set: function(key, val, cb) { var self = this; - //session id - self.sid = null; - //session stored object - self.obj = null; - //this is new session? - self.new = false; - - //sessoin file path - self.path = null; - - //init session object - self.init(req, res); - }; - - SessionParser.prototype = { - init: function(req, res) { - var self = this, - sidKey = "_wsid", - sidVal; - - //Get or Create sid, sid exist in the cookie, read it - var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - - //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - self.new = true; - }; - self.sid = sidVal; - - //Update sessionfile path - self.path = path.join(Settings.sessionDir, self.sid); - } + //Get session object first + self.get(function() { - //Create new session object - , newObj: function(key, cb){ - //Key is offered, return null of this key, else return empty session object - var val = key ? null : {}; + //Add or update key/value in session object + self.obj[key] = val; - this.obj = {}; - cb && cb(val); - return val; - } + fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } - //Get value from session object - , getVal: function(key, cb){ - //key is null, return all the session object - var val = key ? this.obj[key] : this.obj; - cb && cb(val); - return val; - } + cb && cb(self.obj); + }); + }); + } - //Set an key/value pair in session object - , set: function(key, val, cb) { - var self = this; + //Get value from session file + , get: function(key, cb) { + var self = this; + + //The first parameter is callback function + if (key.constructor == Function) { + cb = key; + key = null; + } - //Get session object first - self.get(function() { + //The session object is already loaded + if (self.obj) return self.getVal(key, cb); - //Add or update key/value in session object - self.obj[key] = val; + //It's a new session file, need not to load it from file + if (self.new) return self.newObj(key, cb); - fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + //File operates, a bit of delay + fs.exists(self.path, function(exists) { + if (exists) { + fs.readFile(self.path, function(err, data) { if (err) { console.log(err); return; - } + }; + data = data || "{}"; + self.obj = JSON.parse(data); - cb && cb(self.obj); + return self.getVal(key, cb); }); - }); - } - - //Get value from session file, callbak only need to be appllied once; - , get: function(key, cb) { - var self = this; - - //The first parameter is callback function - if (key.constructor == Function) { - cb = key; - key = null; + } else { + return self.newObj(key, cb); } + }); + } - //The session object is already loaded - if (self.obj) return self.getVal(key, cb); - - //It's a new session file, need not to load it from file - if (self.new) return self.newObj(key, cb); - - //File operates, a bit of delay - fs.exists(self.path, function(exists) { - if (exists) { - fs.readFile(self.path, function(err, data) { - if (err) { - console.log(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - return self.getVal(key, cb); - }); - } else { - return self.newObj(key, cb); - } - }); - } - - }; - -}()); \ No newline at end of file +}; \ No newline at end of file diff --git a/websvr/Settings.js b/websvr/Settings.js index 93b9e42..4661c79 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -15,8 +15,10 @@ var Settings = { //default port of http port: 8054, - //enable debug information output? + //enable debug information output debug: false, + //receive buffer size 32k, i.e.: receive post data from ajax request + bufferSize: 32768, //https https: false, diff --git a/websvr/websvr.js b/websvr/websvr.js index 8617831..51df8d8 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -42,8 +42,10 @@ var Settings = { //default port of http port: 8054, - //enable debug information output? + //enable debug information output debug: false, + //receive buffer size 32k, i.e.: receive post data from ajax request + bufferSize: 32768, //https https: false, @@ -193,8 +195,8 @@ Dual licensed under the MIT and GPL licenses. /* Mapper: Used for Filter & Handler, expression: required parameter -handler: required parameter -options: other optional parameters +handler: required parameter +options: optional parameters */ var Mapper = function(expression, handler, options) { @@ -218,7 +220,7 @@ Mapper.prototype = { //No expression? It's a general filter mapper if (!expression) return true; - switch(expression.constructor) { + switch (expression.constructor) { case String: return req.url.indexOf(expression) > -1; case RegExp: return expression.test(req.url); } @@ -239,158 +241,147 @@ Mapper.prototype = { } } }; -/*RequestParser.js*/ +/*BodyParser.js*/ /* -Request parser, parse the data in request body via +Body parser, parse the data in request body via when parse complete, execute the callback, with response data; */ -var RequestParser; +var BodyParser = function(req, res, callback) { -(function() { + var buffer = new Buffer(Settings.bufferSize); - //TODO: Is there a bug, how about 2 users update a file, what will happened for this buffer? - var MAX_SIZE = 16 * 1024 * 1024, - buffer = new Buffer(MAX_SIZE); + var length = 0, data = ""; - RequestParser = function(req, res, callback) { - var length = 0, data = ""; + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); +}; +/*SessionParser.js*/ +/* +Parse request with session support +*/ +//TODO: Need a child process of clear session +var SessionParser = function(req, res){ + var self = this; - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); - }; + //session id + self.sid = null; + //session stored object + self.obj = null; + //is this new session? + self.new = false; -}()); -/*SessionParser.js*/ - -var SessionParser; + //sessoin file path + self.path = null; -//TODO: Need a child process of clear session -(function() { + //init session object + self.init(req, res); +}; - SessionParser = function(req, res){ - var self = this; +SessionParser.prototype = { + init: function(req, res) { + var self = this, + sidKey = "_wsid", + sidVal; + + //Get or Create sid, sid exist in the cookie, read it + var cookie = req.headers.cookie || ""; + var idx = cookie.indexOf(sidKey + "="); + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + + //Sid doesn't exist, create it + if (idx < 0 || sidVal.length != 32) { + sidVal = Math.uuid(32); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + self.new = true; + }; + self.sid = sidVal; - //session id - self.sid = null; - //session stored object - self.obj = null; - //this is new session? - self.new = false; + //Update sessionfile path + self.path = path.join(Settings.sessionDir, self.sid); + } - //sessoin file path - self.path = null; + //Create new session object + , newObj: function(key, cb){ + //Key is offered, return null of this key, else return empty session object + var val = key ? null : {}; - //init session object - self.init(req, res); - }; + this.obj = {}; + cb && cb(val); + return val; + } - SessionParser.prototype = { - init: function(req, res) { - var self = this, - sidKey = "_wsid", - sidVal; - - //Get or Create sid, sid exist in the cookie, read it - var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); - - //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - self.new = true; - }; - self.sid = sidVal; + //Get value from session object + , getVal: function(key, cb){ + //key is null, return all the session object + var val = key ? this.obj[key] : this.obj; + cb && cb(val); + return val; + } - //Update sessionfile path - self.path = path.join(Settings.sessionDir, self.sid); - } + //Set an key/value pair in session object + , set: function(key, val, cb) { + var self = this; - //Create new session object - , newObj: function(key, cb){ - //Key is offered, return null of this key, else return empty session object - var val = key ? null : {}; + //Get session object first + self.get(function() { - this.obj = {}; - cb && cb(val); - return val; - } + //Add or update key/value in session object + self.obj[key] = val; - //Get value from session object - , getVal: function(key, cb){ - //key is null, return all the session object - var val = key ? this.obj[key] : this.obj; - cb && cb(val); - return val; - } + fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + if (err) { + console.log(err); + return; + } + + cb && cb(self.obj); + }); + }); + } + + //Get value from session file + , get: function(key, cb) { + var self = this; - //Set an key/value pair in session object - , set: function(key, val, cb) { - var self = this; + //The first parameter is callback function + if (key.constructor == Function) { + cb = key; + key = null; + } - //Get session object first - self.get(function() { + //The session object is already loaded + if (self.obj) return self.getVal(key, cb); - //Add or update key/value in session object - self.obj[key] = val; + //It's a new session file, need not to load it from file + if (self.new) return self.newObj(key, cb); - fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + //File operates, a bit of delay + fs.exists(self.path, function(exists) { + if (exists) { + fs.readFile(self.path, function(err, data) { if (err) { console.log(err); return; - } + }; + data = data || "{}"; + self.obj = JSON.parse(data); - cb && cb(self.obj); + return self.getVal(key, cb); }); - }); - } - - //Get value from session file, callbak only need to be appllied once; - , get: function(key, cb) { - var self = this; - - //The first parameter is callback function - if (key.constructor == Function) { - cb = key; - key = null; + } else { + return self.newObj(key, cb); } + }); + } - //The session object is already loaded - if (self.obj) return self.getVal(key, cb); - - //It's a new session file, need not to load it from file - if (self.new) return self.newObj(key, cb); - - //File operates, a bit of delay - fs.exists(self.path, function(exists) { - if (exists) { - fs.readFile(self.path, function(err, data) { - if (err) { - console.log(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - return self.getVal(key, cb); - }); - } else { - return self.newObj(key, cb); - } - }); - } - - }; - -}()); +}; /*Parser.js*/ /* Parser: Functions that Filter and Handler will be called @@ -412,11 +403,11 @@ var Parser = function(req, res, mapper) { parse data in request, this should be done before parse session, because session stored in file */ - var parseRequest = function() { + var parseBody = function() { //need to parse the request? if (mapper.parse && typeof req.body == "undefined") { //Must parser the request first, or the post data will lost; - RequestParser(req, res, function(data) { + BodyParser(req, res, function(data) { req.body = data; parseSession(); }); @@ -448,10 +439,10 @@ var Parser = function(req, res, mapper) { req.files = files; //in fact request will not be parsed again, because body is not undefined - parseRequest(); + parseBody(); }); }else{ - parseRequest(); + parseBody(); }; }; @@ -789,8 +780,7 @@ var Template = (function() { * Licenses: MIT, GPL */ /* -* Define WebSvr -* Export WebSvr +* Define and Export WebSvr */ var WebSvr = module.exports = (function() { From 936e0674da27d386351d00050b9195abe368a567 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 14 Nov 2012 14:30:13 +0800 Subject: [PATCH 038/195] Readme update Readme update --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index fbdce74..18705d4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ WebSvr ============== A simple web server based on node.js Lincenses: MIT, GPL -Version: 0.0.36 +Version: 0.0.4 Features -------------- @@ -122,14 +122,6 @@ Receive upload file (it's a specfic filter) res.end(JSON.stringify(req.files)); }); -Redirect --------------- -Redirect request, try at: http://localhost:8054/redirect - - webSvr.url("redirect", function(req, res) { - res.redirect("/svr/websvr.all.js"); - }); - Template -------------- Render template with params From 58d6095ae905bcc0b97f187e2712742f8bca284f Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 14 Nov 2012 17:59:41 +0800 Subject: [PATCH 039/195] Session: Update Add timeout support --- test/svr/sitetest.js | 3 +- websvr/SessionParser.js | 74 +++++++++++++++++++++++++++++----- websvr/Settings.js | 14 +++---- websvr/websvr.js | 88 +++++++++++++++++++++++++++++++++-------- 4 files changed, 143 insertions(+), 36 deletions(-) diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index ccc41b5..9aa639c 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -21,7 +21,6 @@ var webSvr = new WebSvr({ //tempary upload file stored here, must be end with "/" uploadDir: "tmp/upload/", - listDir: true, debug: true }); @@ -48,7 +47,7 @@ webSvr.filter(/web\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { req.session.get("username", function(val) { - console.log("session", val); + console.log("session username:", val); !val && res.end("You must login, first!"); }); diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index 40e4c4f..307d913 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -1,8 +1,26 @@ +/* +Clear timeout session files +*/ +var SessionCleaner = (function() { + + var list = []; + + var add = function() { + + }; + + return { + add: add + }; + +})(); + + /* Parse request with session support */ //TODO: Need a child process of clear session -var SessionParser = function(req, res){ +var SessionParser = function(req, res) { var self = this; //session id @@ -42,21 +60,44 @@ SessionParser.prototype = { self.path = path.join(Settings.sessionDir, self.sid); } + //Clear session file, not stable, will not remove the expired sessoin file here + /* + , clear: function(key, cb) { + //Key is offered, return null of this key, else return empty session object + var self = this, + val = key ? null : {}; + + fs.unlink(self.path, function (err) { + if (err) console.log(err); + //return an empty sesson object + cb && cb(val); + }); + } + */ + //Create new session object - , newObj: function(key, cb){ + , newObj: function(key, cb) { //Key is offered, return null of this key, else return empty session object - var val = key ? null : {}; + var self = this, + val = key ? null : {}; - this.obj = {}; + self.obj = {}; cb && cb(val); return val; } //Get value from session object - , getVal: function(key, cb){ + , getVal: function(key, cb) { + var self = this; + //key is null, return all the session object - var val = key ? this.obj[key] : this.obj; + var val = key ? self.obj[key] : self.obj; cb && cb(val); + + //update session file accesstime + var time = new Date(); + fs.utimes(self.path, time, time); + return val; } @@ -70,13 +111,19 @@ SessionParser.prototype = { //Add or update key/value in session object self.obj[key] = val; + //Write or modify json file fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { if (err) { console.log(err); return; } - cb && cb(self.obj); + //Update access date time in case of the session file is still existing + var time = new Date(); + fs.utimes(self.path, time, time, function() { + cb && cb(self.obj); + }); + }); }); } @@ -97,9 +144,14 @@ SessionParser.prototype = { //It's a new session file, need not to load it from file if (self.new) return self.newObj(key, cb); - //File operates, a bit of delay - fs.exists(self.path, function(exists) { - if (exists) { + //File operates, will cause delay + fs.stat(self.path, function(err, stats) { + //err: file doesn't exist + if (err) { + return self.newObj(key, cb); + + //session is not timeout + } else if (new Date() - stats.atime <= Settings.sessionAge) { fs.readFile(self.path, function(err, data) { if (err) { console.log(err); @@ -110,6 +162,8 @@ SessionParser.prototype = { return self.getVal(key, cb); }); + + //session is timeout, treat it as new session } else { return self.newObj(key, cb); } diff --git a/websvr/Settings.js b/websvr/Settings.js index 4661c79..d44e66a 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -2,8 +2,6 @@ Configurations */ var Settings = { - version: 0.022, - //root folder of web root: "../", @@ -17,7 +15,7 @@ var Settings = { //enable debug information output debug: false, - //receive buffer size 32k, i.e.: receive post data from ajax request + //receive buffer, default size 32k, i.e.: receive post data from ajax request bufferSize: 32768, //https @@ -29,9 +27,11 @@ var Settings = { //logger file path logger: "./tmp/log.txt", - //session file stored here, must be end with "/" - sessionDir: "./tmp/session/", + //session file stored here + sessionDir: "./tmp/session", + //session timeout, default is 20 minutes, in milliseconds + sessionAge: 10000, - //tempary upload file stored here, must be end with "/" - uploadDir: "./tmp/upload/" + //tempary upload file stored here + uploadDir: "./tmp/upload" }; diff --git a/websvr/websvr.js b/websvr/websvr.js index 51df8d8..9c36270 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -29,8 +29,6 @@ var mime = require("./lib/mime") Configurations */ var Settings = { - version: 0.022, - //root folder of web root: "../", @@ -44,7 +42,7 @@ var Settings = { //enable debug information output debug: false, - //receive buffer size 32k, i.e.: receive post data from ajax request + //receive buffer, default size 32k, i.e.: receive post data from ajax request bufferSize: 32768, //https @@ -56,11 +54,13 @@ var Settings = { //logger file path logger: "./tmp/log.txt", - //session file stored here, must be end with "/" - sessionDir: "./tmp/session/", + //session file stored here + sessionDir: "./tmp/session", + //session timeout, default is 20 minutes, in milliseconds + sessionAge: 10000, - //tempary upload file stored here, must be end with "/" - uploadDir: "./tmp/upload/" + //tempary upload file stored here + uploadDir: "./tmp/upload" }; /*Logger.js*/ @@ -263,11 +263,29 @@ var BodyParser = function(req, res, callback) { }); }; /*SessionParser.js*/ +/* +Clear timeout session files +*/ +var SessionCleaner = (function() { + + var list = []; + + var add = function() { + + }; + + return { + add: add + }; + +})(); + + /* Parse request with session support */ //TODO: Need a child process of clear session -var SessionParser = function(req, res){ +var SessionParser = function(req, res) { var self = this; //session id @@ -307,21 +325,44 @@ SessionParser.prototype = { self.path = path.join(Settings.sessionDir, self.sid); } + //Clear session file, not stable, will not remove the expired sessoin file here + /* + , clear: function(key, cb) { + //Key is offered, return null of this key, else return empty session object + var self = this, + val = key ? null : {}; + + fs.unlink(self.path, function (err) { + if (err) console.log(err); + //return an empty sesson object + cb && cb(val); + }); + } + */ + //Create new session object - , newObj: function(key, cb){ + , newObj: function(key, cb) { //Key is offered, return null of this key, else return empty session object - var val = key ? null : {}; + var self = this, + val = key ? null : {}; - this.obj = {}; + self.obj = {}; cb && cb(val); return val; } //Get value from session object - , getVal: function(key, cb){ + , getVal: function(key, cb) { + var self = this; + //key is null, return all the session object - var val = key ? this.obj[key] : this.obj; + var val = key ? self.obj[key] : self.obj; cb && cb(val); + + //update session file accesstime + var time = new Date(); + fs.utimes(self.path, time, time); + return val; } @@ -335,13 +376,19 @@ SessionParser.prototype = { //Add or update key/value in session object self.obj[key] = val; + //Write or modify json file fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { if (err) { console.log(err); return; } - cb && cb(self.obj); + //Update access date time in case of the session file is still existing + var time = new Date(); + fs.utimes(self.path, time, time, function() { + cb && cb(self.obj); + }); + }); }); } @@ -362,9 +409,14 @@ SessionParser.prototype = { //It's a new session file, need not to load it from file if (self.new) return self.newObj(key, cb); - //File operates, a bit of delay - fs.exists(self.path, function(exists) { - if (exists) { + //File operates, will cause delay + fs.stat(self.path, function(err, stats) { + //err: file doesn't exist + if (err) { + return self.newObj(key, cb); + + //session is not timeout + } else if (new Date() - stats.atime <= Settings.sessionAge) { fs.readFile(self.path, function(err, data) { if (err) { console.log(err); @@ -375,6 +427,8 @@ SessionParser.prototype = { return self.getVal(key, cb); }); + + //session is timeout, treat it as new session } else { return self.newObj(key, cb); } From 16d33983214b815887ec063878a2cbfff1686e7d Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 15 Nov 2012 15:23:04 +0800 Subject: [PATCH 040/195] SessionManager: Add session manager class Clear expired session files Valid session --- websvr/MakeFile.list | 1 + websvr/SessionManager.js | 82 ++++++++++++++++++++++ websvr/SessionParser.js | 66 +++--------------- websvr/Settings.js | 13 +++- websvr/websvr.js | 142 ++++++++++++++++++++++++++------------- 5 files changed, 200 insertions(+), 104 deletions(-) create mode 100644 websvr/SessionManager.js diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list index b0b6bf8..52492a9 100644 --- a/websvr/MakeFile.list +++ b/websvr/MakeFile.list @@ -13,6 +13,7 @@ ref/Math.uuid.js #WebSvr Modules Mapper.js BodyParser.js +SessionManager.js SessionParser.js Parser.js Filter.js diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js new file mode 100644 index 0000000..a4ea033 --- /dev/null +++ b/websvr/SessionManager.js @@ -0,0 +1,82 @@ +/* +SessionManager: +- Clear expired session files +- Valid session +*/ +var SessionManager = (function() { + + //duration time + var gcTime = Settings.sessionAge + Settings.sessionGCT; + + //timer + var timer; + + //session array object, stored with {sid: [update time]}; + var list = {}; + + var getPath = function(sid) { + return path.join(Settings.sessionDir, sid); + }; + + //remove a sesson from list + var remove = function(sid) { + //delete the file + fs.unlink(getPath(sid)); + //remove from list + delete list[sid]; + + console.log("session removed", sid); + }; + + /* + Does session expired? + This session is not in the manage list, add to the list, and treate it as not expired + i.e. The WebSvr is restarted, the session list maybe empty + */ + var isValid = function(sid) { + var now = new Date(); + + !list[sid] && (list[sid] = now); + + return now - list[sid] <= Settings.sessionAge + }; + + /* + + */ + var clean = function() { + for (var sid in list) { + !isValid(sid) && remove(sid); + } + }; + + //update session in list + var update = function(sid, datetime) { + isValid(sid) && (list[sid] = datetime || new Date()); + }; + + var stop = function() { + clearInterval(timer); + timer = null; + }; + + //stop before new session start + var start = function() { + stop(); + timer = setInterval(clean, gcTime); + }; + + //start by default + start(); + + return { + list: list, + update: update, + remove: remove, + isValid: isValid, + getPath: getPath, + start: start, + stop: stop + } + +})(); \ No newline at end of file diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index 307d913..b92ea30 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -1,21 +1,3 @@ -/* -Clear timeout session files -*/ -var SessionCleaner = (function() { - - var list = []; - - var add = function() { - - }; - - return { - add: add - }; - -})(); - - /* Parse request with session support */ @@ -30,9 +12,6 @@ var SessionParser = function(req, res) { //is this new session? self.new = false; - //sessoin file path - self.path = null; - //init session object self.init(req, res); }; @@ -56,25 +35,9 @@ SessionParser.prototype = { }; self.sid = sidVal; - //Update sessionfile path - self.path = path.join(Settings.sessionDir, self.sid); + SessionManager.update(self.sid); } - //Clear session file, not stable, will not remove the expired sessoin file here - /* - , clear: function(key, cb) { - //Key is offered, return null of this key, else return empty session object - var self = this, - val = key ? null : {}; - - fs.unlink(self.path, function (err) { - if (err) console.log(err); - //return an empty sesson object - cb && cb(val); - }); - } - */ - //Create new session object , newObj: function(key, cb) { //Key is offered, return null of this key, else return empty session object @@ -94,10 +57,6 @@ SessionParser.prototype = { var val = key ? self.obj[key] : self.obj; cb && cb(val); - //update session file accesstime - var time = new Date(); - fs.utimes(self.path, time, time); - return val; } @@ -112,18 +71,13 @@ SessionParser.prototype = { self.obj[key] = val; //Write or modify json file - fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { if (err) { console.log(err); return; } - //Update access date time in case of the session file is still existing - var time = new Date(); - fs.utimes(self.path, time, time, function() { - cb && cb(self.obj); - }); - + cb && cb(self.obj); }); }); } @@ -144,15 +98,17 @@ SessionParser.prototype = { //It's a new session file, need not to load it from file if (self.new) return self.newObj(key, cb); + var sessionPath = SessionManager.getPath(self.sid); + //File operates, will cause delay - fs.stat(self.path, function(err, stats) { + fs.exists(sessionPath, function(exists) { //err: file doesn't exist - if (err) { + if (!exists) { return self.newObj(key, cb); - //session is not timeout - } else if (new Date() - stats.atime <= Settings.sessionAge) { - fs.readFile(self.path, function(err, data) { + //session not expired + } else if (SessionManager.isValid(self.sid)) { + fs.readFile(sessionPath, function(err, data) { if (err) { console.log(err); return; @@ -163,7 +119,7 @@ SessionParser.prototype = { return self.getVal(key, cb); }); - //session is timeout, treat it as new session + //session expired, treat it as new session } else { return self.newObj(key, cb); } diff --git a/websvr/Settings.js b/websvr/Settings.js index d44e66a..9e7bf1b 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -29,8 +29,17 @@ var Settings = { //session file stored here sessionDir: "./tmp/session", - //session timeout, default is 20 minutes, in milliseconds - sessionAge: 10000, + /* + Session timeout, in milliseconds. + When session is expired, session file will not deleted. + */ + sessionAge: 1440000, + /* + Session garbage collection time, in milliseconds. + When session expired time is more than (sessionAge + sessionGCT), + then session file will be deleted. + */ + sessionGCT: 3460000, //tempary upload file stored here uploadDir: "./tmp/upload" diff --git a/websvr/websvr.js b/websvr/websvr.js index 9c36270..5249085 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -56,8 +56,17 @@ var Settings = { //session file stored here sessionDir: "./tmp/session", - //session timeout, default is 20 minutes, in milliseconds - sessionAge: 10000, + /* + Session timeout, in milliseconds. + When session is expired, session file will not deleted. + */ + sessionAge: 1440000, + /* + Session garbage collection time, in milliseconds. + When session expired time is more than (sessionAge + sessionGCT), + then session file will be deleted. + */ + sessionGCT: 3460000, //tempary upload file stored here uploadDir: "./tmp/upload" @@ -262,25 +271,90 @@ var BodyParser = function(req, res, callback) { callback(data); }); }; -/*SessionParser.js*/ +/*SessionManager.js*/ /* -Clear timeout session files +SessionManager: +- Clear expired session files +- Valid session */ -var SessionCleaner = (function() { +var SessionManager = (function() { + + //duration time + var gcTime = Settings.sessionAge + Settings.sessionGCT; - var list = []; + //timer + var timer; - var add = function() { + //session array object, stored with {sid: [update time]}; + var list = {}; + var getPath = function(sid) { + return path.join(Settings.sessionDir, sid); }; - return { - add: add + //remove a sesson from list + var remove = function(sid) { + //delete the file + fs.unlink(getPath(sid)); + //remove from list + delete list[sid]; + + console.log("session removed", sid); }; -})(); + /* + Does session expired? + This session is not in the manage list, add to the list, and treate it as not expired + i.e. The WebSvr is restarted, the session list maybe empty + */ + var isValid = function(sid) { + var now = new Date(); + + !list[sid] && (list[sid] = now); + return now - list[sid] <= Settings.sessionAge + }; + + /* + + */ + var clean = function() { + for (var sid in list) { + !isValid(sid) && remove(sid); + } + }; + + //update session in list + var update = function(sid, datetime) { + isValid(sid) && (list[sid] = datetime || new Date()); + }; + + var stop = function() { + clearInterval(timer); + timer = null; + }; + //stop before new session start + var start = function() { + stop(); + timer = setInterval(clean, gcTime); + }; + + //start by default + start(); + + return { + list: list, + update: update, + remove: remove, + isValid: isValid, + getPath: getPath, + start: start, + stop: stop + } + +})(); +/*SessionParser.js*/ /* Parse request with session support */ @@ -295,9 +369,6 @@ var SessionParser = function(req, res) { //is this new session? self.new = false; - //sessoin file path - self.path = null; - //init session object self.init(req, res); }; @@ -321,24 +392,8 @@ SessionParser.prototype = { }; self.sid = sidVal; - //Update sessionfile path - self.path = path.join(Settings.sessionDir, self.sid); - } - - //Clear session file, not stable, will not remove the expired sessoin file here - /* - , clear: function(key, cb) { - //Key is offered, return null of this key, else return empty session object - var self = this, - val = key ? null : {}; - - fs.unlink(self.path, function (err) { - if (err) console.log(err); - //return an empty sesson object - cb && cb(val); - }); + SessionManager.update(self.sid); } - */ //Create new session object , newObj: function(key, cb) { @@ -359,10 +414,6 @@ SessionParser.prototype = { var val = key ? self.obj[key] : self.obj; cb && cb(val); - //update session file accesstime - var time = new Date(); - fs.utimes(self.path, time, time); - return val; } @@ -377,18 +428,13 @@ SessionParser.prototype = { self.obj[key] = val; //Write or modify json file - fs.writeFile(self.path, JSON.stringify(self.obj), function(err) { + fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { if (err) { console.log(err); return; } - //Update access date time in case of the session file is still existing - var time = new Date(); - fs.utimes(self.path, time, time, function() { - cb && cb(self.obj); - }); - + cb && cb(self.obj); }); }); } @@ -409,15 +455,17 @@ SessionParser.prototype = { //It's a new session file, need not to load it from file if (self.new) return self.newObj(key, cb); + var sessionPath = SessionManager.getPath(self.sid); + //File operates, will cause delay - fs.stat(self.path, function(err, stats) { + fs.exists(sessionPath, function(exists) { //err: file doesn't exist - if (err) { + if (!exists) { return self.newObj(key, cb); - //session is not timeout - } else if (new Date() - stats.atime <= Settings.sessionAge) { - fs.readFile(self.path, function(err, data) { + //session not expired + } else if (SessionManager.isValid(self.sid)) { + fs.readFile(sessionPath, function(err, data) { if (err) { console.log(err); return; @@ -428,7 +476,7 @@ SessionParser.prototype = { return self.getVal(key, cb); }); - //session is timeout, treat it as new session + //session expired, treat it as new session } else { return self.newObj(key, cb); } From cb85cc3efe57fa4815bf3a77ecff3e15ba0dcbf0 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 15 Nov 2012 20:34:08 +0800 Subject: [PATCH 041/195] Update websvr/SessionParser.js Remove unuseful comments. --- websvr/SessionParser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index b92ea30..b09ef63 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -1,7 +1,6 @@ /* Parse request with session support */ -//TODO: Need a child process of clear session var SessionParser = function(req, res) { var self = this; From 7153245ebc0d9e6f5d9770ce2d11747c9cb97098 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 15 Nov 2012 20:38:01 +0800 Subject: [PATCH 042/195] Update websvr/SessionManager.js Update comments --- websvr/SessionManager.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index a4ea033..1920d13 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -30,8 +30,8 @@ var SessionManager = (function() { /* Does session expired? - This session is not in the manage list, add to the list, and treate it as not expired - i.e. The WebSvr is restarted, the session list maybe empty + If the session is not in the list, add to the list. + i.e. When WebSvr restarted, session will not expired. */ var isValid = function(sid) { var now = new Date(); @@ -42,9 +42,9 @@ var SessionManager = (function() { }; /* - + Session clean handler */ - var clean = function() { + var cleanHandler = function() { for (var sid in list) { !isValid(sid) && remove(sid); } @@ -63,7 +63,7 @@ var SessionManager = (function() { //stop before new session start var start = function() { stop(); - timer = setInterval(clean, gcTime); + timer = setInterval(cleanHandler, gcTime); }; //start by default From a7b9786fc73c47a055a9a342f2ac689145d24bec Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 16 Nov 2012 16:31:59 +0800 Subject: [PATCH 043/195] Server: Add 304 client-cache support; Server: Add 304 client-cache support; Rename: Make it more readable; --- websvr/Global.js | 5 ++- websvr/Logger.js | 21 +++++----- websvr/Server.js | 24 ++++++++---- websvr/SessionManager.js | 14 +++++-- websvr/SessionParser.js | 5 ++- websvr/Settings.js | 4 +- websvr/websvr.js | 84 ++++++++++++++++++++++++++-------------- 7 files changed, 103 insertions(+), 54 deletions(-) diff --git a/websvr/Global.js b/websvr/Global.js index 6fd0fb0..caa3d9e 100644 --- a/websvr/Global.js +++ b/websvr/Global.js @@ -20,6 +20,9 @@ var fs = require("fs"); var path = require("path"); var qs = require("querystring"); +var http = require("http"); +var https = require("https"); + //Open source library var _ = require("./lib/underscore"); -var mime = require("./lib/mime") \ No newline at end of file +var mime = require("./lib/mime"); \ No newline at end of file diff --git a/websvr/Logger.js b/websvr/Logger.js index 86c8d95..88e08d5 100644 --- a/websvr/Logger.js +++ b/websvr/Logger.js @@ -1,29 +1,32 @@ /* Logger: log sth */ -var Logger = (function(){ +var Logger = (function() { var lineSeparator = "\r\n", indentSeparator = "\t", depth = 9; - var log = function(logObj){ + var log = function(logObj, dep) { + + var depth = dep || depth; var output = new Date() + lineSeparator; - function print(pre, obj){ - if(!obj) return; - for(var key in obj){ - output = output + pre + key + " : " + obj[key] + lineSeparator; - if(typeof obj[key] == "object"){ - (pre.length < depth) && print(pre + indentSeparator, obj[key]); + function print(pre, obj) { + if (!obj) return; + for (var key in obj) { + var val = obj[key]; + output = output + pre + key + " : " + val + lineSeparator; + if (typeof val == "object") { + (pre.length < depth) && print(pre + indentSeparator, val); } } } print(indentSeparator, logObj); - fs.appendFile(Settings.logger, output, function(err){ + fs.appendFile(Settings.logger, output, function(err) { console.log(err); }); }; diff --git a/websvr/Server.js b/websvr/Server.js index 53a71b4..69fd4c0 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -16,6 +16,8 @@ var WebSvr = module.exports = (function() { root, port; + var i = 0; + var fileHandler = function(req, res) { var url = req.url, @@ -33,7 +35,18 @@ var WebSvr = module.exports = (function() { //Is file? Open this file and send to client. if (stat.isFile()) { - writeFile(res, fullPath); + // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 + var cacheTime = new Date(req.headers["if-modified-since"] || 1); + + // The file is modified + if (stat.mtime > cacheTime) { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, fullPath); + // Else send "not modifed" + } else { + res.writeHead(304); + res.end(); + } } //Is Directory? List all the files and folders. @@ -63,6 +76,7 @@ var WebSvr = module.exports = (function() { self.writeFile(res, filePath, cb); }; + //301/302 : move permanently res.redirect = function(url, status) { res.writeHead(status ? status : 302, { "Location": url }); res.end(); @@ -129,10 +143,6 @@ var WebSvr = module.exports = (function() { }); }; - //TODO: Support 301 move permanently - - //TODO: Support 304 client-side cache - self.write403 = function(res) { res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); @@ -153,7 +163,7 @@ var WebSvr = module.exports = (function() { //Create http server if (options.http) { - var httpSvr = require("http").createServer(requestHandler); + var httpSvr = http.createServer(requestHandler); httpSvr.listen(port); console.log("Http server running at" @@ -169,7 +179,7 @@ var WebSvr = module.exports = (function() { var httpsOpts = options.httpsOpts, httpsPort = options.httpsPort; - var httpsSvr = require("https").createServer(httpsOpts, requestHandler); + var httpsSvr = https.createServer(httpsOpts, requestHandler); httpsSvr.listen(httpsPort); console.log("Https server running at" diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index 1920d13..d874962 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -6,7 +6,7 @@ SessionManager: var SessionManager = (function() { //duration time - var gcTime = Settings.sessionAge + Settings.sessionGCT; + var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; //timer var timer; @@ -38,7 +38,7 @@ var SessionManager = (function() { !list[sid] && (list[sid] = now); - return now - list[sid] <= Settings.sessionAge + return now - list[sid] <= Settings.sessionTimeout }; /* @@ -50,9 +50,14 @@ var SessionManager = (function() { } }; - //update session in list + //force update session in list var update = function(sid, datetime) { - isValid(sid) && (list[sid] = datetime || new Date()); + list[sid] = datetime || new Date(); + }; + + //refresh session in list, valid first, if not expired, update the time + var refresh = function(sid, datetime) { + isValid(sid) && update(sid, datetime); }; var stop = function() { @@ -73,6 +78,7 @@ var SessionManager = (function() { list: list, update: update, remove: remove, + refresh: refresh, isValid: isValid, getPath: getPath, start: start, diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index b09ef63..ad03905 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -34,7 +34,7 @@ SessionParser.prototype = { }; self.sid = sidVal; - SessionManager.update(self.sid); + SessionManager.refresh(self.sid); } //Create new session object @@ -77,6 +77,9 @@ SessionParser.prototype = { } cb && cb(self.obj); + + //force update + SessionManager.update(self.sid); }); }); } diff --git a/websvr/Settings.js b/websvr/Settings.js index 9e7bf1b..02df48f 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -33,13 +33,13 @@ var Settings = { Session timeout, in milliseconds. When session is expired, session file will not deleted. */ - sessionAge: 1440000, + sessionTimeout: 1440000, /* Session garbage collection time, in milliseconds. When session expired time is more than (sessionAge + sessionGCT), then session file will be deleted. */ - sessionGCT: 3460000, + sessionGarbage: 3460000, //tempary upload file stored here uploadDir: "./tmp/upload" diff --git a/websvr/websvr.js b/websvr/websvr.js index 5249085..973ed43 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -21,9 +21,12 @@ var fs = require("fs"); var path = require("path"); var qs = require("querystring"); +var http = require("http"); +var https = require("https"); + //Open source library var _ = require("./lib/underscore"); -var mime = require("./lib/mime") +var mime = require("./lib/mime"); /*Settings.js*/ /* Configurations @@ -60,13 +63,13 @@ var Settings = { Session timeout, in milliseconds. When session is expired, session file will not deleted. */ - sessionAge: 1440000, + sessionTimeout: 1440000, /* Session garbage collection time, in milliseconds. When session expired time is more than (sessionAge + sessionGCT), then session file will be deleted. */ - sessionGCT: 3460000, + sessionGarbage: 3460000, //tempary upload file stored here uploadDir: "./tmp/upload" @@ -76,29 +79,32 @@ var Settings = { /* Logger: log sth */ -var Logger = (function(){ +var Logger = (function() { var lineSeparator = "\r\n", indentSeparator = "\t", depth = 9; - var log = function(logObj){ + var log = function(logObj, dep) { + + var depth = dep || depth; var output = new Date() + lineSeparator; - function print(pre, obj){ - if(!obj) return; - for(var key in obj){ - output = output + pre + key + " : " + obj[key] + lineSeparator; - if(typeof obj[key] == "object"){ - (pre.length < depth) && print(pre + indentSeparator, obj[key]); + function print(pre, obj) { + if (!obj) return; + for (var key in obj) { + var val = obj[key]; + output = output + pre + key + " : " + val + lineSeparator; + if (typeof val == "object") { + (pre.length < depth) && print(pre + indentSeparator, val); } } } print(indentSeparator, logObj); - fs.appendFile(Settings.logger, output, function(err){ + fs.appendFile(Settings.logger, output, function(err) { console.log(err); }); }; @@ -280,7 +286,7 @@ SessionManager: var SessionManager = (function() { //duration time - var gcTime = Settings.sessionAge + Settings.sessionGCT; + var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; //timer var timer; @@ -304,29 +310,34 @@ var SessionManager = (function() { /* Does session expired? - This session is not in the manage list, add to the list, and treate it as not expired - i.e. The WebSvr is restarted, the session list maybe empty + If the session is not in the list, add to the list. + i.e. When WebSvr restarted, session will not expired. */ var isValid = function(sid) { var now = new Date(); !list[sid] && (list[sid] = now); - return now - list[sid] <= Settings.sessionAge + return now - list[sid] <= Settings.sessionTimeout }; /* - + Session clean handler */ - var clean = function() { + var cleanHandler = function() { for (var sid in list) { !isValid(sid) && remove(sid); } }; - //update session in list + //force update session in list var update = function(sid, datetime) { - isValid(sid) && (list[sid] = datetime || new Date()); + list[sid] = datetime || new Date(); + }; + + //refresh session in list, valid first, if not expired, update the time + var refresh = function(sid, datetime) { + isValid(sid) && update(sid, datetime); }; var stop = function() { @@ -337,7 +348,7 @@ var SessionManager = (function() { //stop before new session start var start = function() { stop(); - timer = setInterval(clean, gcTime); + timer = setInterval(cleanHandler, gcTime); }; //start by default @@ -347,6 +358,7 @@ var SessionManager = (function() { list: list, update: update, remove: remove, + refresh: refresh, isValid: isValid, getPath: getPath, start: start, @@ -358,7 +370,6 @@ var SessionManager = (function() { /* Parse request with session support */ -//TODO: Need a child process of clear session var SessionParser = function(req, res) { var self = this; @@ -392,7 +403,7 @@ SessionParser.prototype = { }; self.sid = sidVal; - SessionManager.update(self.sid); + SessionManager.refresh(self.sid); } //Create new session object @@ -435,6 +446,9 @@ SessionParser.prototype = { } cb && cb(self.obj); + + //force update + SessionManager.update(self.sid); }); }); } @@ -894,6 +908,8 @@ var WebSvr = module.exports = (function() { root, port; + var i = 0; + var fileHandler = function(req, res) { var url = req.url, @@ -911,7 +927,18 @@ var WebSvr = module.exports = (function() { //Is file? Open this file and send to client. if (stat.isFile()) { - writeFile(res, fullPath); + // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 + var cacheTime = new Date(req.headers["if-modified-since"] || 1); + + // The file is modified + if (stat.mtime > cacheTime) { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, fullPath); + // Else send "not modifed" + } else { + res.writeHead(304); + res.end(); + } } //Is Directory? List all the files and folders. @@ -941,6 +968,7 @@ var WebSvr = module.exports = (function() { self.writeFile(res, filePath, cb); }; + //301/302 : move permanently res.redirect = function(url, status) { res.writeHead(status ? status : 302, { "Location": url }); res.end(); @@ -1007,10 +1035,6 @@ var WebSvr = module.exports = (function() { }); }; - //TODO: Support 301 move permanently - - //TODO: Support 304 client-side cache - self.write403 = function(res) { res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); @@ -1031,7 +1055,7 @@ var WebSvr = module.exports = (function() { //Create http server if (options.http) { - var httpSvr = require("http").createServer(requestHandler); + var httpSvr = http.createServer(requestHandler); httpSvr.listen(port); console.log("Http server running at" @@ -1047,7 +1071,7 @@ var WebSvr = module.exports = (function() { var httpsOpts = options.httpsOpts, httpsPort = options.httpsPort; - var httpsSvr = require("https").createServer(httpsOpts, requestHandler); + var httpsSvr = https.createServer(httpsOpts, requestHandler); httpsSvr.listen(httpsPort); console.log("Https server running at" From d69539dce5695d6e5006f9494660cfa461cd0db1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 16 Nov 2012 19:51:02 +0800 Subject: [PATCH 044/195] Update websvr/websvr.js --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 973ed43..7c74149 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -22,7 +22,7 @@ var path = require("path"); var qs = require("querystring"); var http = require("http"); -var https = require("https"); +var https = require("https"); //Open source library var _ = require("./lib/underscore"); From d20ec8a57db28d15fe9d396f21715fa11e46ec1a Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 16 Nov 2012 19:51:31 +0800 Subject: [PATCH 045/195] Update websvr/Global.js --- websvr/Global.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/Global.js b/websvr/Global.js index caa3d9e..f85a095 100644 --- a/websvr/Global.js +++ b/websvr/Global.js @@ -21,7 +21,7 @@ var path = require("path"); var qs = require("querystring"); var http = require("http"); -var https = require("https"); +var https = require("https"); //Open source library var _ = require("./lib/underscore"); From dfefeac528a43237f061eb2b3526309d1ca5de14 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 15 Nov 2012 20:52:45 +0800 Subject: [PATCH 046/195] Update readme and websvr Update readme and websvr --- README.md | 6 +++--- test/svr/sitetest.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 18705d4..626e5e4 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,9 @@ Render template with params webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); - //render request with session username; - req.session.get(function(session){ - res.render(req, {username: session["username"]}); + //render template with session: { "username" : "admin" } + req.session.get(function(session) { + res.render(req, session); }); }); diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 9aa639c..18b80c1 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -107,9 +107,9 @@ Template: render template with params */ webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); - //render request with session username; + //render template with session: { "username" : "admin" } req.session.get(function(session) { - res.render(req, {username: session["username"]}); + res.render(req, session); }); }); From 14590f18de93b90390e238f1845b7a6e20055cec Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 18 Nov 2012 21:44:17 +0800 Subject: [PATCH 047/195] Update sitetest.js Add new TODO feature --- test/svr/sitetest.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/svr/sitetest.js b/test/svr/sitetest.js index 18b80c1..28ce642 100644 --- a/test/svr/sitetest.js +++ b/test/svr/sitetest.js @@ -46,15 +46,20 @@ Session Filter: protect web/* folder => (validation by session); webSvr.filter(/web\/[\w\.]+/, function(req, res) { //It's not index.htm/login.do, do the session validation if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { + //Once session is get initialized + //TODO: Make sure next req.session.get() will not load session file again. req.session.get("username", function(val) { console.log("session username:", val); !val && res.end("You must login, first!"); + + //Link to next filter + req.filter.next(); }); - } - //Link to next filter - req.filter.next(); + } else { + req.filter.next(); + } }); @@ -109,6 +114,7 @@ webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //render template with session: { "username" : "admin" } req.session.get(function(session) { + //TODO: Change to req.render(session); res.render(req, session); }); }); From 0e698e6ac1c63798282ee530351e139c68c768de Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 23 Nov 2012 11:31:22 +0800 Subject: [PATCH 048/195] 304: Make it controllable Make the client-side cache controllable --- README.md | 1 + test/{start.bat => start.cmd} | 0 websvr/Server.js | 15 ++++++++------- websvr/Settings.js | 4 +++- websvr/{build.bat => build.cmd} | 0 websvr/websvr.js | 18 ++++++++++-------- 6 files changed, 22 insertions(+), 16 deletions(-) rename test/{start.bat => start.cmd} (100%) rename websvr/{build.bat => build.cmd} (100%) diff --git a/README.md b/README.md index 626e5e4..5152012 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Features - Handler: When a request matched a handler, it will returned, only one handler will be executed - Session: Stored in file, with JSON format - File: Support uploading files +- TODO: Custom index page and 404 error pages Start -------------- diff --git a/test/start.bat b/test/start.cmd similarity index 100% rename from test/start.bat rename to test/start.cmd diff --git a/websvr/Server.js b/websvr/Server.js index 69fd4c0..701f196 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -39,13 +39,13 @@ var WebSvr = module.exports = (function() { var cacheTime = new Date(req.headers["if-modified-since"] || 1); // The file is modified - if (stat.mtime > cacheTime) { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, fullPath); - // Else send "not modifed" - } else { + if (Settings.cache && stat.mtime <= cacheTime) { res.writeHead(304); res.end(); + // Else send "not modifed" + } else { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, fullPath); } } @@ -67,7 +67,7 @@ var WebSvr = module.exports = (function() { //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object - res.write = res.writeHead = function() { + res.write = res.writeHead = res.setHeader = function() { console.log("response is already end, response.write ignored!") }; }; @@ -106,7 +106,8 @@ var WebSvr = module.exports = (function() { console.log(err); return; } - res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + res.setHeader("Content-Type", mime.lookup(fullPath)); + res.writeHead(200); res.end(data, "binary"); }); }; diff --git a/websvr/Settings.js b/websvr/Settings.js index 02df48f..ffb5944 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -13,6 +13,8 @@ var Settings = { //default port of http port: 8054, + //enable client-side cache(304)? + cache: true, //enable debug information output debug: false, //receive buffer, default size 32k, i.e.: receive post data from ajax request @@ -43,4 +45,4 @@ var Settings = { //tempary upload file stored here uploadDir: "./tmp/upload" -}; +}; \ No newline at end of file diff --git a/websvr/build.bat b/websvr/build.cmd similarity index 100% rename from websvr/build.bat rename to websvr/build.cmd diff --git a/websvr/websvr.js b/websvr/websvr.js index 7c74149..79ae45f 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -43,6 +43,8 @@ var Settings = { //default port of http port: 8054, + //enable client-side cache(304)? + cache: true, //enable debug information output debug: false, //receive buffer, default size 32k, i.e.: receive post data from ajax request @@ -74,7 +76,6 @@ var Settings = { //tempary upload file stored here uploadDir: "./tmp/upload" }; - /*Logger.js*/ /* Logger: log sth @@ -931,13 +932,13 @@ var WebSvr = module.exports = (function() { var cacheTime = new Date(req.headers["if-modified-since"] || 1); // The file is modified - if (stat.mtime > cacheTime) { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, fullPath); - // Else send "not modifed" - } else { + if (Settings.cache && stat.mtime <= cacheTime) { res.writeHead(304); res.end(); + // Else send "not modifed" + } else { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, fullPath); } } @@ -959,7 +960,7 @@ var WebSvr = module.exports = (function() { //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object - res.write = res.writeHead = function() { + res.write = res.writeHead = res.setHeader = function() { console.log("response is already end, response.write ignored!") }; }; @@ -998,7 +999,8 @@ var WebSvr = module.exports = (function() { console.log(err); return; } - res.writeHead(200, { "Content-Type": mime.lookup(fullPath) }); + res.setHeader("Content-Type", mime.lookup(fullPath)); + res.writeHead(200); res.end(data, "binary"); }); }; From 07bc728b6c08c6452553195679538d3c6a720779 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Mon, 10 Dec 2012 14:43:45 +0800 Subject: [PATCH 049/195] Add building script for linux. --- test/start.sh | 9 +++++++++ websvr/build.cmd | 2 +- websvr/build.sh | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100755 test/start.sh create mode 100755 websvr/build.sh diff --git a/test/start.sh b/test/start.sh new file mode 100755 index 0000000..0bd12dc --- /dev/null +++ b/test/start.sh @@ -0,0 +1,9 @@ +#!/bin/bash +echo "start sitetest" + +while true; do + node svr/sitetest.js + + echo "exit, press any kep to continue..." + read -N1 +done \ No newline at end of file diff --git a/websvr/build.cmd b/websvr/build.cmd index 37566ea..ef87895 100644 --- a/websvr/build.cmd +++ b/websvr/build.cmd @@ -1,5 +1,5 @@ REM Build websvr JS, and keep watching the changes -node tool/Combine.js -i makefile.list -o websvr.js -w +node tool/Combine.js -i MakeFile.list -o websvr.js -w REM Combine complete, Goodbye. pause; \ No newline at end of file diff --git a/websvr/build.sh b/websvr/build.sh new file mode 100755 index 0000000..16cd5e6 --- /dev/null +++ b/websvr/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +echo "Build websvr JS, and keep watching the changes" +node tool/Combine.js -i MakeFile.list -o websvr.js -w + +echo "Combine complete, Goodbye." +sleep 10 \ No newline at end of file From 7b8a3b55ed6c81f6dd49b0374914a4e5d1e5813b Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Mon, 10 Dec 2012 14:44:36 +0800 Subject: [PATCH 050/195] Add building script for linux. --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 79ae45f..cfd4224 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -113,7 +113,7 @@ var Logger = (function() { return { log: log }; })(); -/*ref\Math.uuid.js*/ +/*ref/Math.uuid.js*/ /*! Math.uuid.js (v1.4) http://www.broofa.com From fb5070a9ece5a7013f6187b84681d1ca2ae2cedc Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 11 Dec 2012 17:47:50 +0800 Subject: [PATCH 051/195] Add timestamps on session id. Used for delete garbage session file. --- websvr/MakeFile.list | 3 - websvr/SessionManager.js | 26 ++++++-- websvr/SessionParser.js | 8 +-- websvr/websvr.js | 128 ++++++++------------------------------- 4 files changed, 50 insertions(+), 115 deletions(-) diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list index 52492a9..24ea75b 100644 --- a/websvr/MakeFile.list +++ b/websvr/MakeFile.list @@ -7,9 +7,6 @@ Global.js Settings.js Logger.js -#Reference: JavaScript library -ref/Math.uuid.js - #WebSvr Modules Mapper.js BodyParser.js diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index d874962..ba6f151 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -18,6 +18,26 @@ var SessionManager = (function() { return path.join(Settings.sessionDir, sid); }; + //create a new session id + var create = function() { + //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions + var uuid + = ((+new Date()) >>> 21) //Time stamp, change interval is 0.583 hours, higher 11 bits will be kept + + '-' + + ((Math.random() * 0x40000000 | 0)) //Random 1: Used for distinguish the session + + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session + + //fix the length to 25 + uuid += '0000000000'.substr(0, 25 - uuid.length); + + return uuid; + }; + + //force update session in list + var update = function(sid, datetime) { + list[sid] = datetime || new Date(); + }; + //remove a sesson from list var remove = function(sid) { //delete the file @@ -50,11 +70,6 @@ var SessionManager = (function() { } }; - //force update session in list - var update = function(sid, datetime) { - list[sid] = datetime || new Date(); - }; - //refresh session in list, valid first, if not expired, update the time var refresh = function(sid, datetime) { isValid(sid) && update(sid, datetime); @@ -76,6 +91,7 @@ var SessionManager = (function() { return { list: list, + create: create, update: update, remove: remove, refresh: refresh, diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index ad03905..b940eab 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -23,12 +23,12 @@ SessionParser.prototype = { //Get or Create sid, sid exist in the cookie, read it var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + var idx = cookie.indexOf(sidKey + "="); + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); + if (idx < 0 || sidVal.length != 25) { + sidVal = SessionManager.create(); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); self.new = true; }; diff --git a/websvr/websvr.js b/websvr/websvr.js index cfd4224..7b71897 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -113,100 +113,6 @@ var Logger = (function() { return { log: log }; })(); -/*ref/Math.uuid.js*/ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); - /*Mapper.js*/ /* Mapper: Used for Filter & Handler, @@ -299,6 +205,26 @@ var SessionManager = (function() { return path.join(Settings.sessionDir, sid); }; + //create a new session id + var create = function() { + //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions + var uuid + = ((+new Date()) >>> 21) //Time stamp, change interval is 0.583 hours, higher 11 bits will be kept + + '-' + + ((Math.random() * 0x40000000 | 0)) //Random 1: Used for distinguish the session + + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session + + //fix the length to 25 + uuid += '0000000000'.substr(0, 25 - uuid.length); + + return uuid; + }; + + //force update session in list + var update = function(sid, datetime) { + list[sid] = datetime || new Date(); + }; + //remove a sesson from list var remove = function(sid) { //delete the file @@ -331,11 +257,6 @@ var SessionManager = (function() { } }; - //force update session in list - var update = function(sid, datetime) { - list[sid] = datetime || new Date(); - }; - //refresh session in list, valid first, if not expired, update the time var refresh = function(sid, datetime) { isValid(sid) && update(sid, datetime); @@ -357,6 +278,7 @@ var SessionManager = (function() { return { list: list, + create: create, update: update, remove: remove, refresh: refresh, @@ -393,12 +315,12 @@ SessionParser.prototype = { //Get or Create sid, sid exist in the cookie, read it var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 38)); + var idx = cookie.indexOf(sidKey + "="); + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 32) { - sidVal = Math.uuid(32); + if (idx < 0 || sidVal.length != 25) { + sidVal = SessionManager.create(); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); self.new = true; }; From 5984b0c85fda4ceba94e866c3612aa88cdd3c8fd Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 12 Dec 2012 14:42:19 +0800 Subject: [PATCH 052/195] SessionManager: Add clean function Old session files will be removed when server restarts. --- websvr/SessionManager.js | 41 +++- websvr/test/testSession.js | 41 ++++ websvr/test/testUUID.js | 439 +++++++++++++++++++++++++++++++++++++ websvr/websvr.js | 41 +++- 4 files changed, 550 insertions(+), 12 deletions(-) create mode 100644 websvr/test/testSession.js create mode 100644 websvr/test/testUUID.js diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index ba6f151..10d209e 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -22,13 +22,13 @@ var SessionManager = (function() { var create = function() { //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions var uuid - = ((+new Date()) >>> 21) //Time stamp, change interval is 0.583 hours, higher 11 bits will be kept + = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars + '-' - + ((Math.random() * 0x40000000 | 0)) //Random 1: Used for distinguish the session - + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session + + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars + + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars //fix the length to 25 - uuid += '0000000000'.substr(0, 25 - uuid.length); + uuid += '00000000000000000000'.substr(0, 25 - uuid.length); return uuid; }; @@ -70,6 +70,31 @@ var SessionManager = (function() { } }; + /* + Clean the session in temp folder + */ + var clean = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return console.log(err); + + //converted to minutes + var expire = (+new Date() - Settings.sessionTimeout) / 60000 | 0; + + files.forEach(function(file) { + if (file.length == 25) { + var stamp = parseInt(file.substr(0, file.indexOf('-'))); + + if (stamp) { + //remove the expired session + stamp < expire + ? remove(file) + : console.log("session skipped", file); + } + } + }); + }); + }; + //refresh session in list, valid first, if not expired, update the time var refresh = function(sid, datetime) { isValid(sid) && update(sid, datetime); @@ -82,7 +107,10 @@ var SessionManager = (function() { //stop before new session start var start = function() { + //stop cleanHandler if available stop(); + //clean the old sessions + clean(); timer = setInterval(cleanHandler, gcTime); }; @@ -97,8 +125,9 @@ var SessionManager = (function() { refresh: refresh, isValid: isValid, getPath: getPath, - start: start, - stop: stop + clean: clean, + start: start, + stop: stop } })(); \ No newline at end of file diff --git a/websvr/test/testSession.js b/websvr/test/testSession.js new file mode 100644 index 0000000..d40bb56 --- /dev/null +++ b/websvr/test/testSession.js @@ -0,0 +1,41 @@ +var test = function(msg, func, repeat) { + var t1 = new Date(); + + repeat = repeat || 1000000; + + for (var i = 0; i < repeat; i++) { + func(); + } + + var t2 = new Date(); + + console.log("time used:" , t2 - t1, ", ", msg); +}; + +var create = function() { + //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions + var uuid + = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars + + '-' + + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 9 chars + + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 9 chars + + //fix the length to 25 + //uuid += '0000000000'.substr(0, 25 - uuid.length); + + return uuid; +}; + +test("SessionManager.create: test length", function() { + + var max = 0, min = 1000; + for (var i = 0; i < 1000000; i ++) { + var len = create().length; + + (len > max) && (max = len); + (len < min) && (min = len); + } + + console.log("min:", min, "max:", max); + +}, 1); \ No newline at end of file diff --git a/websvr/test/testUUID.js b/websvr/test/testUUID.js new file mode 100644 index 0000000..c19a541 --- /dev/null +++ b/websvr/test/testUUID.js @@ -0,0 +1,439 @@ +(function() { + // Private array of chars to use + var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + + Math.uuid = function (len, radix) { + var chars = CHARS, uuid = [], i; + radix = radix || chars.length; + + if (len) { + // Compact form + for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; + } else { + // rfc4122, version 4 form + var r; + + // rfc4122 requires these characters + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; + uuid[14] = '4'; + + // Fill in random data. At i==19 set the high bits of clock sequence as + // per rfc4122, sec. 4.1.5 + for (i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | Math.random()*16; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + } + + return uuid.join(''); + }; + + // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance + // by minimizing calls to random() + Math.uuidFast = function() { + var chars = CHARS, uuid = new Array(36), rnd=0, r; + for (var i = 0; i < 36; i++) { + if (i==8 || i==13 || i==18 || i==23) { + uuid[i] = '-'; + } else if (i==14) { + uuid[i] = '4'; + } else { + if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; + r = rnd & 0xf; + rnd = rnd >> 4; + uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; + } + } + return uuid.join(''); + }; + + // A more compact, but less performant, RFC4122v4 solution: + Math.uuidCompact = function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }; +})(); + +/* +* UUID-js: A js library to generate and parse UUIDs, TimeUUIDs and generate +* TimeUUID based on dates for range selections. +* @see http://www.ietf.org/rfc/rfc4122.txt +**/ + +function UUIDjs() { +}; + +UUIDjs.maxFromBits = function(bits) { + return Math.pow(2, bits); +}; + +UUIDjs.limitUI04 = UUIDjs.maxFromBits(4); +UUIDjs.limitUI06 = UUIDjs.maxFromBits(6); +UUIDjs.limitUI08 = UUIDjs.maxFromBits(8); +UUIDjs.limitUI12 = UUIDjs.maxFromBits(12); +UUIDjs.limitUI14 = UUIDjs.maxFromBits(14); +UUIDjs.limitUI16 = UUIDjs.maxFromBits(16); +UUIDjs.limitUI32 = UUIDjs.maxFromBits(32); +UUIDjs.limitUI40 = UUIDjs.maxFromBits(40); +UUIDjs.limitUI48 = UUIDjs.maxFromBits(48); + +UUIDjs.randomUI04 = function() { + return Math.round(Math.random() * UUIDjs.limitUI04); +}; +UUIDjs.randomUI06 = function() { + return Math.round(Math.random() * UUIDjs.limitUI06); +}; +UUIDjs.randomUI08 = function() { + return Math.round(Math.random() * UUIDjs.limitUI08); +}; +UUIDjs.randomUI12 = function() { + return Math.round(Math.random() * UUIDjs.limitUI12); +}; +UUIDjs.randomUI14 = function() { + return Math.round(Math.random() * UUIDjs.limitUI14); +}; +UUIDjs.randomUI16 = function() { + return Math.round(Math.random() * UUIDjs.limitUI16); +}; +UUIDjs.randomUI32 = function() { + return Math.round(Math.random() * UUIDjs.limitUI32); +}; +UUIDjs.randomUI40 = function() { + return (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << 40 - 30)) * (1 << 30); +}; +UUIDjs.randomUI48 = function() { + return (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << 48 - 30)) * (1 << 30); +}; + +UUIDjs.paddedString = function(string, length, z) { + string = String(string); + z = (!z) ? '0' : z; + var i = length - string.length; + for (; i > 0; i >>>= 1, z += z) { + if (i & 1) { + string = z + string; + } + } + return string; +}; + +UUIDjs.prototype.fromParts = function(timeLow, timeMid, timeHiAndVersion, clockSeqHiAndReserved, clockSeqLow, node) { + this.version = (timeHiAndVersion >> 12) & 0xF; + this.hex = UUIDjs.paddedString(timeLow.toString(16), 8) + + '-' + + UUIDjs.paddedString(timeMid.toString(16), 4) + + '-' + + UUIDjs.paddedString(timeHiAndVersion.toString(16), 4) + + '-' + + UUIDjs.paddedString(clockSeqHiAndReserved.toString(16), 2) + + UUIDjs.paddedString(clockSeqLow.toString(16), 2) + + '-' + + UUIDjs.paddedString(node.toString(16), 12); + return this; +}; + +UUIDjs.prototype.toString = function() { + return this.hex; +}; +UUIDjs.prototype.toURN = function() { + return 'urn:uuid:' + this.hex; +}; + +UUIDjs.prototype.toBytes = function() { + var parts = this.hex.split('-'); + var ints = []; + var intPos = 0; + for (var i = 0; i < parts.length; i++) { + for (var j = 0; j < parts[i].length; j+=2) { + ints[intPos++] = parseInt(parts[i].substr(j, 2), 16); + } + } + return ints; +}; + +UUIDjs.prototype.equals = function(uuid) { + if (!(uuid instanceof UUID)) { + return false; + } + if (this.hex !== uuid.hex) { + return false; + } + return true; +}; + +UUIDjs.getTimeFieldValues = function(time) { + var ts = time - Date.UTC(1582, 9, 15); + var hm = ((ts / 0x100000000) * 10000) & 0xFFFFFFF; + return { low: ((ts & 0xFFFFFFF) * 10000) % 0x100000000, + mid: hm & 0xFFFF, hi: hm >>> 16, timestamp: ts }; +}; + +UUIDjs._create4 = function() { + return new UUIDjs().fromParts( + UUIDjs.randomUI32(), + UUIDjs.randomUI16(), + 0x4000 | UUIDjs.randomUI12(), + 0x80 | UUIDjs.randomUI06(), + UUIDjs.randomUI08(), + UUIDjs.randomUI48() + ); +}; + +UUIDjs._create1 = function() { + var now = new Date().getTime(); + var sequence = UUIDjs.randomUI14(); + var node = (UUIDjs.randomUI08() | 1) * 0x10000000000 + UUIDjs.randomUI40(); + var tick = UUIDjs.randomUI04(); + var timestamp = 0; + var timestampRatio = 1/4; + + if (now != timestamp) { + if (now < timestamp) { + sequence++; + } + timestamp = now; + tick = UUIDjs.randomUI04(); + } else if (Math.random() < timestampRatio && tick < 9984) { + tick += 1 + UUIDjs.randomUI04(); + } else { + sequence++; + } + + var tf = UUIDjs.getTimeFieldValues(timestamp); + var tl = tf.low + tick; + var thav = (tf.hi & 0xFFF) | 0x1000; + + sequence &= 0x3FFF; + var cshar = (sequence >>> 8) | 0x80; + var csl = sequence & 0xFF; + + return new UUIDjs().fromParts(tl, tf.mid, thav, cshar, csl, node); +}; + +UUIDjs.create = function(version) { + version = version || 4; + return this['_create' + version](); +}; + +UUIDjs.fromTime = function(time, last) { + last = (!last) ? false : last; + var tf = UUIDjs.getTimeFieldValues(time); + var tl = tf.low; + var thav = (tf.hi & 0xFFF) | 0x1000; // set version '0001' + if (last === false) { + return new UUIDjs().fromParts(tl, tf.mid, thav, 0, 0, 0); + } else { + return new UUIDjs().fromParts(tl, tf.mid, thav, 0x80 | UUIDjs.limitUI06, UUIDjs.limitUI08 - 1, UUIDjs.limitUI48 - 1); + } +}; + +UUIDjs.firstFromTime = function(time) { + return UUIDjs.fromTime(time, false); +}; +UUIDjs.lastFromTime = function(time) { + return UUIDjs.fromTime(time, true); +}; + +UUIDjs.fromURN = function(strId) { + var r, p = /^(?:urn:uuid:|\{)?([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})(?:\})?$/i; + if ((r = p.exec(strId))) { + return new UUIDjs().fromParts(parseInt(r[1], 16), parseInt(r[2], 16), + parseInt(r[3], 16), parseInt(r[4], 16), + parseInt(r[5], 16), parseInt(r[6], 16)); + } + return null; +}; + +UUIDjs.fromBytes = function(ints) { + if (ints.length < 5) { + return null; + } + var str = ''; + var pos = 0; + var parts = [4, 2, 2, 2, 6]; + for (var i = 0; i < parts.length; i++) { + for (var j = 0; j < parts[i]; j++) { + var octet = ints[pos++].toString(16); + if (octet.length == 1) { + octet = '0' + octet; + } + str += octet; + } + if (parts[i] !== 6) { + str += '-'; + } + } + return UUIDjs.fromURN(str); +}; + +UUIDjs.fromBinary = function(binary) { + var ints = []; + for (var i = 0; i < binary.length; i++) { + ints[i] = binary.charCodeAt(i); + if (ints[i] > 255 || ints[i] < 0) { + throw new Error('Unexpected byte in binary data.'); + } + } + return UUIDjs.fromBytes(ints); +}; + +// Aliases to support legacy code. Do not use these when writing new code as +// they may be removed in future versions! +UUIDjs.new = function() { + return this.create(4); +}; +UUIDjs.newTS = function() { + return this.create(1); +}; + +module.exports = UUIDjs; + +var test = function(msg, func) { + var t1 = new Date(); + + for (var i = 0; i < 1000000; i++) { + func(); + } + + var t2 = new Date(); + + console.log("time used:" , t2 - t1, ", ", msg); +}; + +//Conver to array will make it slower; +var CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +var CHARSArr = CHARS.split(''); + +var SCHARS = "0123456789ABCDEF"; +var SCHARSArr = SCHARS.split(''); + +console.log("Testing long chars:"); + +test("Math.uuid(32)", function() { + Math.uuid(32); +}); + +test("Math.uuidFast", function() { + Math.uuidFast(); +}); + +test("UUIDjs.create", function() { + UUIDjs.create(); +}); + +var createUUID = function() { + var create = function() { + return (Math.random() * 0x10000 | 0).toString(16); + }; + + var uuid = new Array(8); + for (var i = 0; i < 8; i++) { + uuid[i] = (Math.random() * 0x10000 | 0).toString(16); + } +}; + +test("createUUID", function() { + createUUID(); +}); + +test("UUID fixed array", function() { + //Using fixed size array will be more faster than "new Array()" + var uuid = new Array(32); + for (var i = 0; i < 32; i++) { + uuid[i] = CHARS[0 | Math.random() * 36]; + } + uuid = uuid.join(); +}); + +test("UUID string plus", function() { + var uuid = ""; + for (var i = 0; i < 32; i++) { + uuid += CHARS[0 | Math.random() * 36]; + } +}); + +test("UUIDArr string plus", function() { + var uuid = ""; + for (var i = 0; i < 32; i++) { + uuid += CHARSArr[0 | Math.random() * 36]; + } +}); + +test("UUIDArr fixed array", function() { + //Using fixed size array will be more faster than "new Array()" + var uuid = new Array(32); + for (var i = 0; i < 32; i++) { + uuid[i] = CHARSArr[0 | Math.random() * 36]; + } + uuid = uuid.join(); +}); + +console.log("\r\n\r\nTesting short chars:"); + +test("UUID fixed array", function() { + //Using fixed size array will be more faster than "new Array()" + var uuid = new Array(32); + for (var i = 0; i < 32; i++) { + uuid[i] = SCHARS[0 | Math.random() * 16]; + } + uuid = uuid.join(); +}); + +test("UUID string plus", function() { + var uuid = ""; + for (var i = 0; i < 32; i++) { + uuid += SCHARS[0 | Math.random() * 16]; + } +}); + +test("UUIDArr string plus", function() { + var uuid = ""; + for (var i = 0; i < 32; i++) { + uuid += SCHARSArr[0 | Math.random() * 16]; + } +}); + +test("UUIDArr fixed array", function() { + //Using fixed size array will be more faster than "new Array()" + var uuid = new Array(32); + for (var i = 0; i < 32; i++) { + uuid[i] = SCHARSArr[0 | Math.random() * 16]; + } + uuid = uuid.join(); +}); + +//interval is 4 hours +test("UUIDArr fixed array final", function() { + var suuid = (+new Date()) / 14400000; + + var uuid = new Array(32); + for (var i = 0; i < 32; i++) { + uuid[i] = SCHARSArr[0 | Math.random() * 16]; + } + + suuid += "-" + uuid.join(''); +}); + +test("Custom session id, with full Date", function() { + var uuid + = (+new Date()) //Time stamp, change interval is 0.583 hours, higher 11 bits will be kept + + '-' + + ((Math.random() * 0x40000000 | 0)) //Random 1: Used for distinguish the session + + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session + + uuid += '0000000000'.substr(0, 25 - uuid.length); +}); + +test("Custom session id, with short Date", function() { + var uuid + = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 0.583 hours, higher 11 bits will be kept + + '-' + + ((Math.random() * 0x40000000 | 0)) //Random 1: Used for distinguish the session + + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session + + uuid += '0000000000'.substr(0, 25 - uuid.length); +}); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 7b71897..1f06985 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -209,13 +209,13 @@ var SessionManager = (function() { var create = function() { //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions var uuid - = ((+new Date()) >>> 21) //Time stamp, change interval is 0.583 hours, higher 11 bits will be kept + = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars + '-' - + ((Math.random() * 0x40000000 | 0)) //Random 1: Used for distinguish the session - + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session + + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars + + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars //fix the length to 25 - uuid += '0000000000'.substr(0, 25 - uuid.length); + uuid += '00000000000000000000'.substr(0, 25 - uuid.length); return uuid; }; @@ -257,6 +257,31 @@ var SessionManager = (function() { } }; + /* + Clean the session in temp folder + */ + var clean = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return console.log(err); + + //converted to minutes + var expire = (+new Date() - Settings.sessionTimeout) / 60000 | 0; + + files.forEach(function(file) { + if (file.length == 25) { + var stamp = parseInt(file.substr(0, file.indexOf('-'))); + + if (stamp) { + //remove the expired session + stamp < expire + ? remove(file) + : console.log("session skipped", file); + } + } + }); + }); + }; + //refresh session in list, valid first, if not expired, update the time var refresh = function(sid, datetime) { isValid(sid) && update(sid, datetime); @@ -269,7 +294,10 @@ var SessionManager = (function() { //stop before new session start var start = function() { + //stop cleanHandler if available stop(); + //clean the old sessions + clean(); timer = setInterval(cleanHandler, gcTime); }; @@ -284,8 +312,9 @@ var SessionManager = (function() { refresh: refresh, isValid: isValid, getPath: getPath, - start: start, - stop: stop + clean: clean, + start: start, + stop: stop } })(); From 431befee1c03fd0023462bd904f1bc5821ef9d72 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 12 Dec 2012 15:06:52 +0800 Subject: [PATCH 053/195] SessionManager: Fix bug Start the sesion garbage collection in Server.js, in order to load the user custom settings. --- websvr/Server.js | 6 ++++++ websvr/SessionManager.js | 3 --- websvr/websvr.js | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/websvr/Server.js b/websvr/Server.js index 701f196..1d8d79f 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -195,6 +195,12 @@ var WebSvr = module.exports = (function() { if (!options.debug) { console.log = function(){}; } + + /* + init modules + */ + //Start session garbage collection + SessionManager.start(); }; //Public: close http server; diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index 10d209e..088dc58 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -114,9 +114,6 @@ var SessionManager = (function() { timer = setInterval(cleanHandler, gcTime); }; - //start by default - start(); - return { list: list, create: create, diff --git a/websvr/websvr.js b/websvr/websvr.js index 1f06985..b11f542 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -301,9 +301,6 @@ var SessionManager = (function() { timer = setInterval(cleanHandler, gcTime); }; - //start by default - start(); - return { list: list, create: create, @@ -1039,6 +1036,12 @@ var WebSvr = module.exports = (function() { if (!options.debug) { console.log = function(){}; } + + /* + init modules + */ + //Start session garbage collection + SessionManager.start(); }; //Public: close http server; From d9c53b69c6177cf372d49b8a906dc5182cd84c8c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 12 Dec 2012 22:44:00 +0800 Subject: [PATCH 054/195] SessionManager Update --- websvr/SessionManager.js | 2 +- websvr/test/testSession.js | 22 +++++++++++++++++++++- websvr/websvr.js | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index 088dc58..9dd17da 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -78,7 +78,7 @@ var SessionManager = (function() { if (err) return console.log(err); //converted to minutes - var expire = (+new Date() - Settings.sessionTimeout) / 60000 | 0; + var expire = (+new Date() - gcTime) / 60000 | 0; files.forEach(function(file) { if (file.length == 25) { diff --git a/websvr/test/testSession.js b/websvr/test/testSession.js index d40bb56..be17148 100644 --- a/websvr/test/testSession.js +++ b/websvr/test/testSession.js @@ -38,4 +38,24 @@ test("SessionManager.create: test length", function() { console.log("min:", min, "max:", max); -}, 1); \ No newline at end of file +}, 1); + +test("Test parse Int string: | 0", function() { + var rnd = "18999334.3415921676"; + rnd = rnd | 0; +}); + +test("Test parse Int string: parseInt", function() { + var rnd = "18999334.3415921676"; + rnd = parseInt(rnd); +}); + +test("Test parse Int float: | 0", function() { + var rnd = 18999334.3415921676; + rnd = rnd | 0; +}); + +test("Test parse Int float: parseInt", function() { + var rnd = 18999334.3415921676; + rnd = parseInt(rnd); +}); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index b11f542..60105e7 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -265,7 +265,7 @@ var SessionManager = (function() { if (err) return console.log(err); //converted to minutes - var expire = (+new Date() - Settings.sessionTimeout) / 60000 | 0; + var expire = (+new Date() - gcTime) / 60000 | 0; files.forEach(function(file) { if (file.length == 25) { From 3701a16373ff2db2113eeda4e318c3e8aef1b6a6 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 13 Dec 2012 13:22:05 +0800 Subject: [PATCH 055/195] Update reference. Make it simple. --- websvr/Global.js | 25 ++++---- websvr/MakeFile.list | 2 + websvr/Utility.js | 15 +++++ websvr/ref/Math.uuid.js | 92 ----------------------------- websvr/{ => test}/lib/underscore.js | 0 websvr/test/testExtend.js | 58 ++++++++++++++++++ websvr/websvr.js | 41 +++++++++---- 7 files changed, 115 insertions(+), 118 deletions(-) create mode 100644 websvr/Utility.js delete mode 100644 websvr/ref/Math.uuid.js rename websvr/{ => test}/lib/underscore.js (100%) create mode 100644 websvr/test/testExtend.js diff --git a/websvr/Global.js b/websvr/Global.js index f85a095..c7efccb 100644 --- a/websvr/Global.js +++ b/websvr/Global.js @@ -1,21 +1,21 @@ /* -* Description: WebSvr -* Author: Kris Zhang -* Lincense: MIT, GPL -* Included Projects: -- Formidable: Support uploading files, integrate +* Description: node-websvr +* Author: Kris Zhang +* Licenses: MIT, GPL +* Project url: https://github.com/newghost/node-websvr +* +* Referenced projects: +* Formidable: Support uploading files, integrate https://github.com/felixge/node-formidable/ -- Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -- Underscore: Add underscore a utility-belt library for JavaScript +* Underscore: Add underscore a utility-belt library for JavaScript https://github.com/documentcloud/underscore -- MIME: content-type in header +* MIME: content-type in header https://github.com/broofa/node-mime -- template: Template Engine +* template: Template Engine https://github.com/olado/doT */ -//Node library +//Node libraries var fs = require("fs"); var path = require("path"); var qs = require("querystring"); @@ -23,6 +23,5 @@ var qs = require("querystring"); var http = require("http"); var https = require("https"); -//Open source library -var _ = require("./lib/underscore"); +//Open source libraries var mime = require("./lib/mime"); \ No newline at end of file diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list index 24ea75b..0f5c246 100644 --- a/websvr/MakeFile.list +++ b/websvr/MakeFile.list @@ -4,6 +4,8 @@ #Global object and description Global.js +Utility.js + Settings.js Logger.js diff --git a/websvr/Utility.js b/websvr/Utility.js new file mode 100644 index 0000000..58ae734 --- /dev/null +++ b/websvr/Utility.js @@ -0,0 +1,15 @@ +/* +* Utility +*/ +var _ = { + //extend object to target + extend: function(tar, obj) { + if (!obj) return; + + for (var key in obj) { + tar[key] = obj[key]; + } + + return tar; + } +}; \ No newline at end of file diff --git a/websvr/ref/Math.uuid.js b/websvr/ref/Math.uuid.js deleted file mode 100644 index 38a6271..0000000 --- a/websvr/ref/Math.uuid.js +++ /dev/null @@ -1,92 +0,0 @@ -/*! -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com - -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * - * USAGE: Math.uuid(length, radix) - * length - the desired number of characters - * radix - the number of allowable values for each character. - * - * EXAMPLES: - * // No arguments - returns RFC4122, version 4 ID - * >>> Math.uuid() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * // One argument - returns ID of the specified length - * >>> Math.uuid(15) // 15 character ID (default base=62) - * "VcydxgltxrVZSTV" - * - * // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62) - * >>> Math.uuid(8, 2) // 8 character ID (base=2) - * "01001010" - * >>> Math.uuid(8, 10) // 8 character ID (base=10) - * "47473046" - * >>> Math.uuid(8, 16) // 8 character ID (base=16) - * "098F4D35" - */ -(function() { - // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - - Math.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; - radix = radix || chars.length; - - if (len) { - // Compact form - for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix]; - } else { - // rfc4122, version 4 form - var r; - - // rfc4122 requires these characters - uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; - uuid[14] = '4'; - - // Fill in random data. At i==19 set the high bits of clock sequence as - // per rfc4122, sec. 4.1.5 - for (i = 0; i < 36; i++) { - if (!uuid[i]) { - r = 0 | Math.random()*16; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - } - - return uuid.join(''); - }; - - // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance - // by minimizing calls to random() - Math.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; - for (var i = 0; i < 36; i++) { - if (i==8 || i==13 || i==18 || i==23) { - uuid[i] = '-'; - } else if (i==14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0; - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); - }; - - // A more compact, but less performant, RFC4122v4 solution: - Math.uuidCompact = function() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - }; -})(); diff --git a/websvr/lib/underscore.js b/websvr/test/lib/underscore.js similarity index 100% rename from websvr/lib/underscore.js rename to websvr/test/lib/underscore.js diff --git a/websvr/test/testExtend.js b/websvr/test/testExtend.js new file mode 100644 index 0000000..7691e59 --- /dev/null +++ b/websvr/test/testExtend.js @@ -0,0 +1,58 @@ +var test = function(msg, func, repeat) { + var t1 = new Date(); + + repeat = repeat || 1000000; + + for (var i = 0; i < repeat; i++) { + func(); + } + + var t2 = new Date(); + + console.log("time used:" , t2 - t1, ", ", msg); +}; + +var extend = function(tar, obj) { + if (!obj) return; + + for (var key in obj) { + tar[key] = obj[key]; + } + + return tar; +}; + +var _ = require("./lib/underscore"); + +//test method +test("Extend by _.extend:", function() { + var tar = { + a: 1, b: "2", c: "3", + e: { i: "5", j: "6" }, + f: { x: {}, y: [] } + }; + + var obj = { + b: 123, + f: { x: [1]} + }; + + _.extend(tar, obj); + //console.log(tar, obj); +}); + +test("Extend by simple func:", function() { + var tar = { + a: 1, b: "2", c: "3", + e: { i: "5", j: "6" }, + f: { x: {}, y: [] } + }; + + var obj = { + b: 123, + f: { x: [1]} + }; + + extend(tar, obj); + //console.log(tar, obj); +}); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 60105e7..a48b786 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1,22 +1,22 @@ /*Global.js*/ /* -* Description: WebSvr -* Author: Kris Zhang -* Lincense: MIT, GPL -* Included Projects: -- Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -- Formidable: Support uploading files, integrate +* Description: node-websvr +* Author: Kris Zhang +* Licenses: MIT, GPL +* Project url: https://github.com/newghost/node-websvr +* +* Referenced projects: +* Formidable: Support uploading files, integrate https://github.com/felixge/node-formidable/ -- Underscore: Add underscore a utility-belt library for JavaScript +* Underscore: Add underscore a utility-belt library for JavaScript https://github.com/documentcloud/underscore -- MIME: content-type in header +* MIME: content-type in header https://github.com/broofa/node-mime -- template: Template Engine +* template: Template Engine https://github.com/olado/doT */ -//Node library +//Node libraries var fs = require("fs"); var path = require("path"); var qs = require("querystring"); @@ -24,9 +24,24 @@ var qs = require("querystring"); var http = require("http"); var https = require("https"); -//Open source library -var _ = require("./lib/underscore"); +//Open source libraries var mime = require("./lib/mime"); +/*Utility.js*/ +/* +* Utility +*/ +var _ = { + //extend object to target + extend: function(tar, obj) { + if (!obj) return; + + for (var key in obj) { + tar[key] = obj[key]; + } + + return tar; + } +}; /*Settings.js*/ /* Configurations From d8dbbd3c24268a5ad7bb76eb94224ae1f79451c9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 13 Dec 2012 13:30:13 +0800 Subject: [PATCH 056/195] Rename test site folder Rename test site folder --- {test => site}/start.cmd | 0 {test => site}/start.sh | 16 ++++++++-------- {test => site}/svr/cert/certificate.pem | 0 {test => site}/svr/cert/privatekey.pem | 0 {test => site}/svr/sitetest.js | 0 {test => site}/tmp/session/readme.txt | 0 {test => site}/tmp/upload/readme.txt | 0 {test => site}/web/index.htm | 0 {test => site}/web/setting.htm | 0 {test => site}/web/template.node | 0 10 files changed, 8 insertions(+), 8 deletions(-) rename {test => site}/start.cmd (100%) rename {test => site}/start.sh (94%) mode change 100755 => 100644 rename {test => site}/svr/cert/certificate.pem (100%) rename {test => site}/svr/cert/privatekey.pem (100%) rename {test => site}/svr/sitetest.js (100%) rename {test => site}/tmp/session/readme.txt (100%) rename {test => site}/tmp/upload/readme.txt (100%) rename {test => site}/web/index.htm (100%) rename {test => site}/web/setting.htm (100%) rename {test => site}/web/template.node (100%) diff --git a/test/start.cmd b/site/start.cmd similarity index 100% rename from test/start.cmd rename to site/start.cmd diff --git a/test/start.sh b/site/start.sh old mode 100755 new mode 100644 similarity index 94% rename from test/start.sh rename to site/start.sh index 0bd12dc..8a1f459 --- a/test/start.sh +++ b/site/start.sh @@ -1,9 +1,9 @@ -#!/bin/bash -echo "start sitetest" - -while true; do - node svr/sitetest.js - - echo "exit, press any kep to continue..." - read -N1 +#!/bin/bash +echo "start sitetest" + +while true; do + node svr/sitetest.js + + echo "exit, press any kep to continue..." + read -N1 done \ No newline at end of file diff --git a/test/svr/cert/certificate.pem b/site/svr/cert/certificate.pem similarity index 100% rename from test/svr/cert/certificate.pem rename to site/svr/cert/certificate.pem diff --git a/test/svr/cert/privatekey.pem b/site/svr/cert/privatekey.pem similarity index 100% rename from test/svr/cert/privatekey.pem rename to site/svr/cert/privatekey.pem diff --git a/test/svr/sitetest.js b/site/svr/sitetest.js similarity index 100% rename from test/svr/sitetest.js rename to site/svr/sitetest.js diff --git a/test/tmp/session/readme.txt b/site/tmp/session/readme.txt similarity index 100% rename from test/tmp/session/readme.txt rename to site/tmp/session/readme.txt diff --git a/test/tmp/upload/readme.txt b/site/tmp/upload/readme.txt similarity index 100% rename from test/tmp/upload/readme.txt rename to site/tmp/upload/readme.txt diff --git a/test/web/index.htm b/site/web/index.htm similarity index 100% rename from test/web/index.htm rename to site/web/index.htm diff --git a/test/web/setting.htm b/site/web/setting.htm similarity index 100% rename from test/web/setting.htm rename to site/web/setting.htm diff --git a/test/web/template.node b/site/web/template.node similarity index 100% rename from test/web/template.node rename to site/web/template.node From 1468fb309cc0e5dd53618313f364133103ad8037 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 14 Dec 2012 10:03:06 +0800 Subject: [PATCH 057/195] Server Add defaultPage options. Rename test site folder. --- {site => website}/start.cmd | 0 {site => website}/start.sh | 0 {site => website}/svr/cert/certificate.pem | 0 {site => website}/svr/cert/privatekey.pem | 0 {site => website}/svr/sitetest.js | 2 + {site => website}/tmp/session/readme.txt | 0 {site => website}/tmp/upload/readme.txt | 0 {site => website}/web/index.htm | 0 {site => website}/web/setting.htm | 0 {site => website}/web/template.node | 0 websvr/Server.js | 85 +++++++++++++++------ websvr/Settings.js | 3 + websvr/websvr.js | 88 ++++++++++++++++------ 13 files changed, 128 insertions(+), 50 deletions(-) rename {site => website}/start.cmd (100%) rename {site => website}/start.sh (100%) rename {site => website}/svr/cert/certificate.pem (100%) rename {site => website}/svr/cert/privatekey.pem (100%) rename {site => website}/svr/sitetest.js (96%) rename {site => website}/tmp/session/readme.txt (100%) rename {site => website}/tmp/upload/readme.txt (100%) rename {site => website}/web/index.htm (100%) rename {site => website}/web/setting.htm (100%) rename {site => website}/web/template.node (100%) diff --git a/site/start.cmd b/website/start.cmd similarity index 100% rename from site/start.cmd rename to website/start.cmd diff --git a/site/start.sh b/website/start.sh similarity index 100% rename from site/start.sh rename to website/start.sh diff --git a/site/svr/cert/certificate.pem b/website/svr/cert/certificate.pem similarity index 100% rename from site/svr/cert/certificate.pem rename to website/svr/cert/certificate.pem diff --git a/site/svr/cert/privatekey.pem b/website/svr/cert/privatekey.pem similarity index 100% rename from site/svr/cert/privatekey.pem rename to website/svr/cert/privatekey.pem diff --git a/site/svr/sitetest.js b/website/svr/sitetest.js similarity index 96% rename from site/svr/sitetest.js rename to website/svr/sitetest.js index 28ce642..eed79d0 100644 --- a/site/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -15,6 +15,8 @@ var webSvr = new WebSvr({ cert: require("fs").readFileSync("svr/cert/certificate.pem") }, + //defaultPage: "index.htm", + //Change the default locations of tmp session and upload files //session file stored here, must be end with "/" sessionDir: "tmp/session/", diff --git a/site/tmp/session/readme.txt b/website/tmp/session/readme.txt similarity index 100% rename from site/tmp/session/readme.txt rename to website/tmp/session/readme.txt diff --git a/site/tmp/upload/readme.txt b/website/tmp/upload/readme.txt similarity index 100% rename from site/tmp/upload/readme.txt rename to website/tmp/upload/readme.txt diff --git a/site/web/index.htm b/website/web/index.htm similarity index 100% rename from site/web/index.htm rename to website/web/index.htm diff --git a/site/web/setting.htm b/website/web/setting.htm similarity index 100% rename from site/web/setting.htm rename to website/web/setting.htm diff --git a/site/web/template.node b/website/web/template.node similarity index 100% rename from site/web/template.node rename to website/web/template.node diff --git a/websvr/Server.js b/websvr/Server.js index 1d8d79f..a907502 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -16,8 +16,6 @@ var WebSvr = module.exports = (function() { root, port; - var i = 0; - var fileHandler = function(req, res) { var url = req.url, @@ -28,35 +26,72 @@ var WebSvr = module.exports = (function() { var fullPath = path.join(root, url); - fs.stat(fullPath, function(err, stat) { + //Handle path + var handlePath = function(phyPath) { + fs.stat(phyPath, function(err, stat) { + + //Consider as file not found + if (err) return self.write404(res); - //Consider as file not found - if (err) return self.write404(res); + //Is file? Open this file and send to client. + if (stat.isFile()) { + // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 + var cacheTime = new Date(req.headers["if-modified-since"] || 1); - //Is file? Open this file and send to client. - if (stat.isFile()) { - // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 - var cacheTime = new Date(req.headers["if-modified-since"] || 1); + // The file is modified + if (Settings.cache && stat.mtime <= cacheTime) { + res.writeHead(304); + res.end(); - // The file is modified - if (Settings.cache && stat.mtime <= cacheTime) { - res.writeHead(304); - res.end(); - // Else send "not modifed" - } else { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, fullPath); + // Else send "not modifed" + } else { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, phyPath); + } + } + + //Is Directory? + else if (stat.isDirectory()) { + handleDefault(phyPath); + } + + //Or write the 404 pages + else { + self.write404(res); } - } - //Is Directory? List all the files and folders. - else if (stat.isDirectory()) { - options.listDir - ? ListDir.list(req, res, fullPath) - : self.write403(res); + }); + }; + + //List all the files and folders. + var handleDir = function(dirPath) { + options.listDir + ? ListDir.list(req, res, dirPath) + : self.write403(res); + }; + + //Handle default page + var handleDefault = function(dirPath) { + var defaultPage = options.defaultPage; + + if (defaultPage) { + var defaultPath = path.join(dirPath, defaultPage); + + fs.exists(defaultPath, function (exists) { + //If page exists hanle it again + if (exists) { + handlePath(defaultPath); + //If page doesn't exist hanlde the dir again + } else { + handleDir(dirPath); + } + }); + } else { + handleDir(dirPath); } + }; - }); + handlePath(fullPath); }; var requestHandler = function(req, res) { @@ -125,7 +160,7 @@ var WebSvr = module.exports = (function() { //Logger self.log = Logger.log; - //Get a fullpath of a request + //Get a full path of a request self.getFullPath = function(filePath) { return path.join(root, filePath); }; diff --git a/websvr/Settings.js b/websvr/Settings.js index ffb5944..b261824 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -20,6 +20,9 @@ var Settings = { //receive buffer, default size 32k, i.e.: receive post data from ajax request bufferSize: 32768, + //default pages, only one is supported + defaultPage: "index.html", + //https https: false, //default port of https diff --git a/websvr/websvr.js b/websvr/websvr.js index a48b786..e5955df 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -65,6 +65,9 @@ var Settings = { //receive buffer, default size 32k, i.e.: receive post data from ajax request bufferSize: 32768, + //default pages, only one is supported + defaultPage: "index.html", + //https https: false, //default port of https @@ -872,8 +875,6 @@ var WebSvr = module.exports = (function() { root, port; - var i = 0; - var fileHandler = function(req, res) { var url = req.url, @@ -884,35 +885,72 @@ var WebSvr = module.exports = (function() { var fullPath = path.join(root, url); - fs.stat(fullPath, function(err, stat) { + //Handle path + var handlePath = function(phyPath) { + fs.stat(phyPath, function(err, stat) { - //Consider as file not found - if (err) return self.write404(res); + //Consider as file not found + if (err) return self.write404(res); - //Is file? Open this file and send to client. - if (stat.isFile()) { - // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 - var cacheTime = new Date(req.headers["if-modified-since"] || 1); + //Is file? Open this file and send to client. + if (stat.isFile()) { + // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 + var cacheTime = new Date(req.headers["if-modified-since"] || 1); - // The file is modified - if (Settings.cache && stat.mtime <= cacheTime) { - res.writeHead(304); - res.end(); - // Else send "not modifed" - } else { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, fullPath); + // The file is modified + if (Settings.cache && stat.mtime <= cacheTime) { + res.writeHead(304); + res.end(); + + // Else send "not modifed" + } else { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, phyPath); + } + } + + //Is Directory? + else if (stat.isDirectory()) { + handleDefault(phyPath); + } + + //Or write the 404 pages + else { + self.write404(res); } - } - //Is Directory? List all the files and folders. - else if (stat.isDirectory()) { - options.listDir - ? ListDir.list(req, res, fullPath) - : self.write403(res); + }); + }; + + //List all the files and folders. + var handleDir = function(dirPath) { + options.listDir + ? ListDir.list(req, res, dirPath) + : self.write403(res); + }; + + //Handle default page + var handleDefault = function(dirPath) { + var defaultPage = options.defaultPage; + + if (defaultPage) { + var defaultPath = path.join(dirPath, defaultPage); + + fs.exists(defaultPath, function (exists) { + //If page exists hanle it again + if (exists) { + handlePath(defaultPath); + //If page doesn't exist hanlde the dir again + } else { + handleDir(dirPath); + } + }); + } else { + handleDir(dirPath); } + }; - }); + handlePath(fullPath); }; var requestHandler = function(req, res) { @@ -981,7 +1019,7 @@ var WebSvr = module.exports = (function() { //Logger self.log = Logger.log; - //Get a fullpath of a request + //Get a full path of a request self.getFullPath = function(filePath) { return path.join(root, filePath); }; From 8bdf45d177524ce0fb37650131c7de641456f2df Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 14 Dec 2012 10:38:54 +0800 Subject: [PATCH 058/195] DefaultPage: Update Fix bug for loading static resources --- websvr/Server.js | 15 ++++++++++----- websvr/test/testExtend.js | 10 ++++++++++ websvr/websvr.js | 15 ++++++++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/websvr/Server.js b/websvr/Server.js index a907502..8901b5b 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -80,6 +80,11 @@ var WebSvr = module.exports = (function() { fs.exists(defaultPath, function (exists) { //If page exists hanle it again if (exists) { + //In order to make it as a dir path for loading static resources + if (url[url.length - 1] != '/') { + return res.redirect(url + '/'); + } + handlePath(defaultPath); //If page doesn't exist hanlde the dir again } else { @@ -149,13 +154,13 @@ var WebSvr = module.exports = (function() { //Explose API //Filter - self.filter = Filter.filter; - self.file = Filter.file; + self.filter = Filter.filter; + self.file = Filter.file; //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; //Logger self.log = Logger.log; diff --git a/websvr/test/testExtend.js b/websvr/test/testExtend.js index 7691e59..624e242 100644 --- a/websvr/test/testExtend.js +++ b/websvr/test/testExtend.js @@ -55,4 +55,14 @@ test("Extend by simple func:", function() { extend(tar, obj); //console.log(tar, obj); +}); + +test("String operate by []:", function() { + var dirPath = 'www/abc/login/'; + dirPath[dirPath.length - 1] == '/'; +}); + +test("String operate by charAt:", function() { + var dirPath = 'www/abc/login/'; + dirPath.charAt(dirPath.length - 1) == '/'; }); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index e5955df..502cb09 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -939,6 +939,11 @@ var WebSvr = module.exports = (function() { fs.exists(defaultPath, function (exists) { //If page exists hanle it again if (exists) { + //In order to make it as a dir path for loading static resources + if (url[url.length - 1] != '/') { + return res.redirect(url + '/'); + } + handlePath(defaultPath); //If page doesn't exist hanlde the dir again } else { @@ -1008,13 +1013,13 @@ var WebSvr = module.exports = (function() { //Explose API //Filter - self.filter = Filter.filter; - self.file = Filter.file; + self.filter = Filter.filter; + self.file = Filter.file; //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; //Logger self.log = Logger.log; From 091ff63dcb0694e878fc45799388bc907c3d63b7 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 14 Dec 2012 15:55:34 +0800 Subject: [PATCH 059/195] TestCase Update testcase; --- websvr/test/lib/test.js | 22 ++++ websvr/test/lib/uuid.js | 233 ++++++++++++++++++++++++++++++++++ websvr/test/testExtend.js | 14 +-- websvr/test/testSession.js | 14 +-- websvr/test/testUUID.js | 249 +------------------------------------ 5 files changed, 260 insertions(+), 272 deletions(-) create mode 100644 websvr/test/lib/test.js create mode 100644 websvr/test/lib/uuid.js diff --git a/websvr/test/lib/test.js b/websvr/test/lib/test.js new file mode 100644 index 0000000..6678a12 --- /dev/null +++ b/websvr/test/lib/test.js @@ -0,0 +1,22 @@ +//Test method +var os = require("os"); + +var test = module.exports = function(msg, func, repeat) { + + var t1 = new Date(), + m1 = os.freemem(); + + repeat = repeat || 1000000; + + for (var i = 0; i < repeat; i++) { + func(); + } + + var t2 = new Date(), + m2 = os.freemem(); + + console.log("\r\n" + msg); + console.log(" time used:", t2 - t1); + console.log(" memo used:", m1 - m2); + +}; \ No newline at end of file diff --git a/websvr/test/lib/uuid.js b/websvr/test/lib/uuid.js new file mode 100644 index 0000000..f616d52 --- /dev/null +++ b/websvr/test/lib/uuid.js @@ -0,0 +1,233 @@ +/* + * UUID-js: A js library to generate and parse UUIDs, TimeUUIDs and generate + * TimeUUID based on dates for range selections. + * @see http://www.ietf.org/rfc/rfc4122.txt + **/ + +function UUIDjs() { +}; + +UUIDjs.maxFromBits = function(bits) { + return Math.pow(2, bits); +}; + +UUIDjs.limitUI04 = UUIDjs.maxFromBits(4); +UUIDjs.limitUI06 = UUIDjs.maxFromBits(6); +UUIDjs.limitUI08 = UUIDjs.maxFromBits(8); +UUIDjs.limitUI12 = UUIDjs.maxFromBits(12); +UUIDjs.limitUI14 = UUIDjs.maxFromBits(14); +UUIDjs.limitUI16 = UUIDjs.maxFromBits(16); +UUIDjs.limitUI32 = UUIDjs.maxFromBits(32); +UUIDjs.limitUI40 = UUIDjs.maxFromBits(40); +UUIDjs.limitUI48 = UUIDjs.maxFromBits(48); + +UUIDjs.randomUI04 = function() { + return Math.round(Math.random() * UUIDjs.limitUI04); +}; +UUIDjs.randomUI06 = function() { + return Math.round(Math.random() * UUIDjs.limitUI06); +}; +UUIDjs.randomUI08 = function() { + return Math.round(Math.random() * UUIDjs.limitUI08); +}; +UUIDjs.randomUI12 = function() { + return Math.round(Math.random() * UUIDjs.limitUI12); +}; +UUIDjs.randomUI14 = function() { + return Math.round(Math.random() * UUIDjs.limitUI14); +}; +UUIDjs.randomUI16 = function() { + return Math.round(Math.random() * UUIDjs.limitUI16); +}; +UUIDjs.randomUI32 = function() { + return Math.round(Math.random() * UUIDjs.limitUI32); +}; +UUIDjs.randomUI40 = function() { + return (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << 40 - 30)) * (1 << 30); +}; +UUIDjs.randomUI48 = function() { + return (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << 48 - 30)) * (1 << 30); +}; + +UUIDjs.paddedString = function(string, length, z) { + string = String(string); + z = (!z) ? '0' : z; + var i = length - string.length; + for (; i > 0; i >>>= 1, z += z) { + if (i & 1) { + string = z + string; + } + } + return string; +}; + +UUIDjs.prototype.fromParts = function(timeLow, timeMid, timeHiAndVersion, clockSeqHiAndReserved, clockSeqLow, node) { + this.version = (timeHiAndVersion >> 12) & 0xF; + this.hex = UUIDjs.paddedString(timeLow.toString(16), 8) + + '-' + + UUIDjs.paddedString(timeMid.toString(16), 4) + + '-' + + UUIDjs.paddedString(timeHiAndVersion.toString(16), 4) + + '-' + + UUIDjs.paddedString(clockSeqHiAndReserved.toString(16), 2) + + UUIDjs.paddedString(clockSeqLow.toString(16), 2) + + '-' + + UUIDjs.paddedString(node.toString(16), 12); + return this; +}; + +UUIDjs.prototype.toString = function() { + return this.hex; +}; +UUIDjs.prototype.toURN = function() { + return 'urn:uuid:' + this.hex; +}; + +UUIDjs.prototype.toBytes = function() { + var parts = this.hex.split('-'); + var ints = []; + var intPos = 0; + for (var i = 0; i < parts.length; i++) { + for (var j = 0; j < parts[i].length; j+=2) { + ints[intPos++] = parseInt(parts[i].substr(j, 2), 16); + } + } + return ints; +}; + +UUIDjs.prototype.equals = function(uuid) { + if (!(uuid instanceof UUID)) { + return false; + } + if (this.hex !== uuid.hex) { + return false; + } + return true; +}; + +UUIDjs.getTimeFieldValues = function(time) { + var ts = time - Date.UTC(1582, 9, 15); + var hm = ((ts / 0x100000000) * 10000) & 0xFFFFFFF; + return { low: ((ts & 0xFFFFFFF) * 10000) % 0x100000000, + mid: hm & 0xFFFF, hi: hm >>> 16, timestamp: ts }; +}; + +UUIDjs._create4 = function() { + return new UUIDjs().fromParts( + UUIDjs.randomUI32(), + UUIDjs.randomUI16(), + 0x4000 | UUIDjs.randomUI12(), + 0x80 | UUIDjs.randomUI06(), + UUIDjs.randomUI08(), + UUIDjs.randomUI48() + ); +}; + +UUIDjs._create1 = function() { + var now = new Date().getTime(); + var sequence = UUIDjs.randomUI14(); + var node = (UUIDjs.randomUI08() | 1) * 0x10000000000 + UUIDjs.randomUI40(); + var tick = UUIDjs.randomUI04(); + var timestamp = 0; + var timestampRatio = 1/4; + + if (now != timestamp) { + if (now < timestamp) { + sequence++; + } + timestamp = now; + tick = UUIDjs.randomUI04(); + } else if (Math.random() < timestampRatio && tick < 9984) { + tick += 1 + UUIDjs.randomUI04(); + } else { + sequence++; + } + + var tf = UUIDjs.getTimeFieldValues(timestamp); + var tl = tf.low + tick; + var thav = (tf.hi & 0xFFF) | 0x1000; + + sequence &= 0x3FFF; + var cshar = (sequence >>> 8) | 0x80; + var csl = sequence & 0xFF; + + return new UUIDjs().fromParts(tl, tf.mid, thav, cshar, csl, node); +}; + +UUIDjs.create = function(version) { + version = version || 4; + return this['_create' + version](); +}; + +UUIDjs.fromTime = function(time, last) { + last = (!last) ? false : last; + var tf = UUIDjs.getTimeFieldValues(time); + var tl = tf.low; + var thav = (tf.hi & 0xFFF) | 0x1000; // set version '0001' + if (last === false) { + return new UUIDjs().fromParts(tl, tf.mid, thav, 0, 0, 0); + } else { + return new UUIDjs().fromParts(tl, tf.mid, thav, 0x80 | UUIDjs.limitUI06, UUIDjs.limitUI08 - 1, UUIDjs.limitUI48 - 1); + } +}; + +UUIDjs.firstFromTime = function(time) { + return UUIDjs.fromTime(time, false); +}; +UUIDjs.lastFromTime = function(time) { + return UUIDjs.fromTime(time, true); +}; + +UUIDjs.fromURN = function(strId) { + var r, p = /^(?:urn:uuid:|\{)?([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})(?:\})?$/i; + if ((r = p.exec(strId))) { + return new UUIDjs().fromParts(parseInt(r[1], 16), parseInt(r[2], 16), + parseInt(r[3], 16), parseInt(r[4], 16), + parseInt(r[5], 16), parseInt(r[6], 16)); + } + return null; +}; + +UUIDjs.fromBytes = function(ints) { + if (ints.length < 5) { + return null; + } + var str = ''; + var pos = 0; + var parts = [4, 2, 2, 2, 6]; + for (var i = 0; i < parts.length; i++) { + for (var j = 0; j < parts[i]; j++) { + var octet = ints[pos++].toString(16); + if (octet.length == 1) { + octet = '0' + octet; + } + str += octet; + } + if (parts[i] !== 6) { + str += '-'; + } + } + return UUIDjs.fromURN(str); +}; + +UUIDjs.fromBinary = function(binary) { + var ints = []; + for (var i = 0; i < binary.length; i++) { + ints[i] = binary.charCodeAt(i); + if (ints[i] > 255 || ints[i] < 0) { + throw new Error('Unexpected byte in binary data.'); + } + } + return UUIDjs.fromBytes(ints); +}; + +// Aliases to support legacy code. Do not use these when writing new code as +// they may be removed in future versions! +UUIDjs.new = function() { + return this.create(4); +}; +UUIDjs.newTS = function() { + return this.create(1); +}; + +module.exports = UUIDjs; diff --git a/websvr/test/testExtend.js b/websvr/test/testExtend.js index 624e242..05ce3bb 100644 --- a/websvr/test/testExtend.js +++ b/websvr/test/testExtend.js @@ -1,16 +1,4 @@ -var test = function(msg, func, repeat) { - var t1 = new Date(); - - repeat = repeat || 1000000; - - for (var i = 0; i < repeat; i++) { - func(); - } - - var t2 = new Date(); - - console.log("time used:" , t2 - t1, ", ", msg); -}; +var test = require("./lib/test"); var extend = function(tar, obj) { if (!obj) return; diff --git a/websvr/test/testSession.js b/websvr/test/testSession.js index be17148..002279b 100644 --- a/websvr/test/testSession.js +++ b/websvr/test/testSession.js @@ -1,16 +1,4 @@ -var test = function(msg, func, repeat) { - var t1 = new Date(); - - repeat = repeat || 1000000; - - for (var i = 0; i < repeat; i++) { - func(); - } - - var t2 = new Date(); - - console.log("time used:" , t2 - t1, ", ", msg); -}; +var test = require("./lib/test"); var create = function() { //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions diff --git a/websvr/test/testUUID.js b/websvr/test/testUUID.js index c19a541..9c4d378 100644 --- a/websvr/test/testUUID.js +++ b/websvr/test/testUUID.js @@ -1,3 +1,6 @@ +var UUIDjs = require("./lib/uuid"), + test = require("./lib/test"); + (function() { // Private array of chars to use var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); @@ -58,252 +61,6 @@ }; })(); -/* -* UUID-js: A js library to generate and parse UUIDs, TimeUUIDs and generate -* TimeUUID based on dates for range selections. -* @see http://www.ietf.org/rfc/rfc4122.txt -**/ - -function UUIDjs() { -}; - -UUIDjs.maxFromBits = function(bits) { - return Math.pow(2, bits); -}; - -UUIDjs.limitUI04 = UUIDjs.maxFromBits(4); -UUIDjs.limitUI06 = UUIDjs.maxFromBits(6); -UUIDjs.limitUI08 = UUIDjs.maxFromBits(8); -UUIDjs.limitUI12 = UUIDjs.maxFromBits(12); -UUIDjs.limitUI14 = UUIDjs.maxFromBits(14); -UUIDjs.limitUI16 = UUIDjs.maxFromBits(16); -UUIDjs.limitUI32 = UUIDjs.maxFromBits(32); -UUIDjs.limitUI40 = UUIDjs.maxFromBits(40); -UUIDjs.limitUI48 = UUIDjs.maxFromBits(48); - -UUIDjs.randomUI04 = function() { - return Math.round(Math.random() * UUIDjs.limitUI04); -}; -UUIDjs.randomUI06 = function() { - return Math.round(Math.random() * UUIDjs.limitUI06); -}; -UUIDjs.randomUI08 = function() { - return Math.round(Math.random() * UUIDjs.limitUI08); -}; -UUIDjs.randomUI12 = function() { - return Math.round(Math.random() * UUIDjs.limitUI12); -}; -UUIDjs.randomUI14 = function() { - return Math.round(Math.random() * UUIDjs.limitUI14); -}; -UUIDjs.randomUI16 = function() { - return Math.round(Math.random() * UUIDjs.limitUI16); -}; -UUIDjs.randomUI32 = function() { - return Math.round(Math.random() * UUIDjs.limitUI32); -}; -UUIDjs.randomUI40 = function() { - return (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << 40 - 30)) * (1 << 30); -}; -UUIDjs.randomUI48 = function() { - return (0 | Math.random() * (1 << 30)) + (0 | Math.random() * (1 << 48 - 30)) * (1 << 30); -}; - -UUIDjs.paddedString = function(string, length, z) { - string = String(string); - z = (!z) ? '0' : z; - var i = length - string.length; - for (; i > 0; i >>>= 1, z += z) { - if (i & 1) { - string = z + string; - } - } - return string; -}; - -UUIDjs.prototype.fromParts = function(timeLow, timeMid, timeHiAndVersion, clockSeqHiAndReserved, clockSeqLow, node) { - this.version = (timeHiAndVersion >> 12) & 0xF; - this.hex = UUIDjs.paddedString(timeLow.toString(16), 8) - + '-' - + UUIDjs.paddedString(timeMid.toString(16), 4) - + '-' - + UUIDjs.paddedString(timeHiAndVersion.toString(16), 4) - + '-' - + UUIDjs.paddedString(clockSeqHiAndReserved.toString(16), 2) - + UUIDjs.paddedString(clockSeqLow.toString(16), 2) - + '-' - + UUIDjs.paddedString(node.toString(16), 12); - return this; -}; - -UUIDjs.prototype.toString = function() { - return this.hex; -}; -UUIDjs.prototype.toURN = function() { - return 'urn:uuid:' + this.hex; -}; - -UUIDjs.prototype.toBytes = function() { - var parts = this.hex.split('-'); - var ints = []; - var intPos = 0; - for (var i = 0; i < parts.length; i++) { - for (var j = 0; j < parts[i].length; j+=2) { - ints[intPos++] = parseInt(parts[i].substr(j, 2), 16); - } - } - return ints; -}; - -UUIDjs.prototype.equals = function(uuid) { - if (!(uuid instanceof UUID)) { - return false; - } - if (this.hex !== uuid.hex) { - return false; - } - return true; -}; - -UUIDjs.getTimeFieldValues = function(time) { - var ts = time - Date.UTC(1582, 9, 15); - var hm = ((ts / 0x100000000) * 10000) & 0xFFFFFFF; - return { low: ((ts & 0xFFFFFFF) * 10000) % 0x100000000, - mid: hm & 0xFFFF, hi: hm >>> 16, timestamp: ts }; -}; - -UUIDjs._create4 = function() { - return new UUIDjs().fromParts( - UUIDjs.randomUI32(), - UUIDjs.randomUI16(), - 0x4000 | UUIDjs.randomUI12(), - 0x80 | UUIDjs.randomUI06(), - UUIDjs.randomUI08(), - UUIDjs.randomUI48() - ); -}; - -UUIDjs._create1 = function() { - var now = new Date().getTime(); - var sequence = UUIDjs.randomUI14(); - var node = (UUIDjs.randomUI08() | 1) * 0x10000000000 + UUIDjs.randomUI40(); - var tick = UUIDjs.randomUI04(); - var timestamp = 0; - var timestampRatio = 1/4; - - if (now != timestamp) { - if (now < timestamp) { - sequence++; - } - timestamp = now; - tick = UUIDjs.randomUI04(); - } else if (Math.random() < timestampRatio && tick < 9984) { - tick += 1 + UUIDjs.randomUI04(); - } else { - sequence++; - } - - var tf = UUIDjs.getTimeFieldValues(timestamp); - var tl = tf.low + tick; - var thav = (tf.hi & 0xFFF) | 0x1000; - - sequence &= 0x3FFF; - var cshar = (sequence >>> 8) | 0x80; - var csl = sequence & 0xFF; - - return new UUIDjs().fromParts(tl, tf.mid, thav, cshar, csl, node); -}; - -UUIDjs.create = function(version) { - version = version || 4; - return this['_create' + version](); -}; - -UUIDjs.fromTime = function(time, last) { - last = (!last) ? false : last; - var tf = UUIDjs.getTimeFieldValues(time); - var tl = tf.low; - var thav = (tf.hi & 0xFFF) | 0x1000; // set version '0001' - if (last === false) { - return new UUIDjs().fromParts(tl, tf.mid, thav, 0, 0, 0); - } else { - return new UUIDjs().fromParts(tl, tf.mid, thav, 0x80 | UUIDjs.limitUI06, UUIDjs.limitUI08 - 1, UUIDjs.limitUI48 - 1); - } -}; - -UUIDjs.firstFromTime = function(time) { - return UUIDjs.fromTime(time, false); -}; -UUIDjs.lastFromTime = function(time) { - return UUIDjs.fromTime(time, true); -}; - -UUIDjs.fromURN = function(strId) { - var r, p = /^(?:urn:uuid:|\{)?([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{2})([0-9a-f]{2})-([0-9a-f]{12})(?:\})?$/i; - if ((r = p.exec(strId))) { - return new UUIDjs().fromParts(parseInt(r[1], 16), parseInt(r[2], 16), - parseInt(r[3], 16), parseInt(r[4], 16), - parseInt(r[5], 16), parseInt(r[6], 16)); - } - return null; -}; - -UUIDjs.fromBytes = function(ints) { - if (ints.length < 5) { - return null; - } - var str = ''; - var pos = 0; - var parts = [4, 2, 2, 2, 6]; - for (var i = 0; i < parts.length; i++) { - for (var j = 0; j < parts[i]; j++) { - var octet = ints[pos++].toString(16); - if (octet.length == 1) { - octet = '0' + octet; - } - str += octet; - } - if (parts[i] !== 6) { - str += '-'; - } - } - return UUIDjs.fromURN(str); -}; - -UUIDjs.fromBinary = function(binary) { - var ints = []; - for (var i = 0; i < binary.length; i++) { - ints[i] = binary.charCodeAt(i); - if (ints[i] > 255 || ints[i] < 0) { - throw new Error('Unexpected byte in binary data.'); - } - } - return UUIDjs.fromBytes(ints); -}; - -// Aliases to support legacy code. Do not use these when writing new code as -// they may be removed in future versions! -UUIDjs.new = function() { - return this.create(4); -}; -UUIDjs.newTS = function() { - return this.create(1); -}; - -module.exports = UUIDjs; - -var test = function(msg, func) { - var t1 = new Date(); - - for (var i = 0; i < 1000000; i++) { - func(); - } - - var t2 = new Date(); - - console.log("time used:" , t2 - t1, ", ", msg); -}; - //Conver to array will make it slower; var CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; var CHARSArr = CHARS.split(''); From 292472d8e6108a74947c7cd405bc96de2fb23248 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 22 Jan 2013 16:44:48 +0800 Subject: [PATCH 060/195] Update 1) Add more test cases. 2) Implement Logger. --- websvr/Filter.js | 2 +- websvr/Handler.js | 4 +- websvr/ListDir.js | 4 +- websvr/Logger.js | 40 +- websvr/Parser.js | 2 +- websvr/Server.js | 18 +- websvr/SessionManager.js | 6 +- websvr/SessionParser.js | 4 +- websvr/Settings.js | 2 +- websvr/Template.js | 4 +- websvr/test/lib/MIME.types | 105 +++ websvr/test/lib/MIMETYPES.js | 901 ++++++++++++++++++ websvr/test/lib/mime.js | 93 ++ websvr/test/lib/types.js | 52 + websvr/test/lib/types/mime.types | 1510 ++++++++++++++++++++++++++++++ websvr/test/lib/types/node.types | 54 ++ websvr/test/testFormat.js | 19 + websvr/test/testMIME.js | 44 + websvr/websvr.js | 86 +- 19 files changed, 2880 insertions(+), 70 deletions(-) create mode 100644 websvr/test/lib/MIME.types create mode 100644 websvr/test/lib/MIMETYPES.js create mode 100644 websvr/test/lib/mime.js create mode 100644 websvr/test/lib/types.js create mode 100644 websvr/test/lib/types/mime.types create mode 100644 websvr/test/lib/types/node.types create mode 100644 websvr/test/testFormat.js create mode 100644 websvr/test/testMIME.js diff --git a/websvr/Filter.js b/websvr/Filter.js index 81edf2e..a0319b2 100644 --- a/websvr/Filter.js +++ b/websvr/Filter.js @@ -69,7 +69,7 @@ FilterChain.prototype = { }, options); */ if (mapper.match(req)) { - console.log("filter matched", self.idx, mapper.expression, req.url); + Logger.debug("filter matched", self.idx, mapper.expression, req.url); //filter matched, parse the request and then execute it Parser(req, res, mapper); diff --git a/websvr/Handler.js b/websvr/Handler.js index eda6d19..04d9edd 100644 --- a/websvr/Handler.js +++ b/websvr/Handler.js @@ -43,10 +43,10 @@ var Handler; var mapper = handlers[i]; if (mapper.match(req)) { - console.log("handler matched", i, mapper.expression, req.url); + Logger.debug("handler matched", i, mapper.expression, req.url); var handler = mapper.handler, - type = handler.constructor.name; + type = handler.constructor.name; switch(type) { //function: treated it as custom function handler diff --git a/websvr/ListDir.js b/websvr/ListDir.js index da788cd..98518db 100644 --- a/websvr/ListDir.js +++ b/websvr/ListDir.js @@ -53,7 +53,7 @@ var ListDir = (function() { fs.readdir(dir, function(err, files) { if (err) { listEnd(); - console.log(err); + Logger.debug(err); return; } @@ -69,7 +69,7 @@ var ListDir = (function() { cur++; if (err) { - console.log(err); + Logger.debug(err); }else{ res.write( date(stat.mtime) diff --git a/websvr/Logger.js b/websvr/Logger.js index 88e08d5..2077de8 100644 --- a/websvr/Logger.js +++ b/websvr/Logger.js @@ -3,15 +3,13 @@ Logger: log sth */ var Logger = (function() { - var lineSeparator = "\r\n", + var lineSeparator = "\r\n", indentSeparator = "\t", depth = 9; - var log = function(logObj, dep) { - - var depth = dep || depth; - - var output = new Date() + lineSeparator; + var write = function(logObj, dep) { + var depth = dep || depth, + output = new Date() + lineSeparator; function print(pre, obj) { if (!obj) return; @@ -27,10 +25,36 @@ var Logger = (function() { print(indentSeparator, logObj); fs.appendFile(Settings.logger, output, function(err) { - console.log(err); + log(err); }); }; - return { log: log }; + /* + Currnetly it's equal to console.log + */ + var log = function() { + console.log.apply(console, arguments); + }; + + /* + Add data before log information + */ + var debug = function() { + //diable console.log information + if (!Settings.debug) { + return; + } + + var d = new Date().toString(); + + Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + console.log.apply(console, arguments); + }; + + return { + log: log + , write: write + , debug: debug + }; })(); \ No newline at end of file diff --git a/websvr/Parser.js b/websvr/Parser.js index 361e8ec..9b3e17c 100644 --- a/websvr/Parser.js +++ b/websvr/Parser.js @@ -45,7 +45,7 @@ var Parser = function(req, res, mapper) { form.parse(req, function(err, fields, files) { if (err) { - console.log(err); + Logger.debug(err); return; }; diff --git a/websvr/Server.js b/websvr/Server.js index 8901b5b..87c7bad 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -108,7 +108,7 @@ var WebSvr = module.exports = (function() { endFn.apply(res, arguments); //Rewirte write/writeHead on response object res.write = res.writeHead = res.setHeader = function() { - console.log("response is already end, response.write ignored!") + Logger.debug("response is already end, response.write ignored!") }; }; @@ -143,7 +143,7 @@ var WebSvr = module.exports = (function() { var writeFile = function(res, fullPath) { fs.readFile(fullPath, function(err, data) { if (err) { - console.log(err); + Logger.log(err); return; } res.setHeader("Content-Type", mime.lookup(fullPath)); @@ -162,9 +162,6 @@ var WebSvr = module.exports = (function() { self.post = Handler.post; self.session = Handler.session; - //Logger - self.log = Logger.log; - //Get a full path of a request self.getFullPath = function(filePath) { return path.join(root, filePath); @@ -207,7 +204,7 @@ var WebSvr = module.exports = (function() { var httpSvr = http.createServer(requestHandler); httpSvr.listen(port); - console.log("Http server running at" + Logger.log("Http server running at" ,"Root:", root ,"Port:", port ); @@ -223,7 +220,7 @@ var WebSvr = module.exports = (function() { var httpsSvr = https.createServer(httpsOpts, requestHandler); httpsSvr.listen(httpsPort); - console.log("Https server running at" + Logger.log("Https server running at" ,"Root:", root ,"Port:", httpsPort ); @@ -231,11 +228,6 @@ var WebSvr = module.exports = (function() { self.httpsSvr = httpsSvr; } - //diable console.log information - if (!options.debug) { - console.log = function(){}; - } - /* init modules */ @@ -245,7 +237,7 @@ var WebSvr = module.exports = (function() { //Public: close http server; self.close = function() { - self.httpSvr && self.httpSvr.close(); + self.httpSvr && self.httpSvr.close(); self.httpsSvr && self.httpsSvr.close(); }; diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index 9dd17da..064392c 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -45,7 +45,7 @@ var SessionManager = (function() { //remove from list delete list[sid]; - console.log("session removed", sid); + Logger.log("session removed", sid); }; /* @@ -75,7 +75,7 @@ var SessionManager = (function() { */ var clean = function() { fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return console.log(err); + if (err) return Logger.log(err); //converted to minutes var expire = (+new Date() - gcTime) / 60000 | 0; @@ -88,7 +88,7 @@ var SessionManager = (function() { //remove the expired session stamp < expire ? remove(file) - : console.log("session skipped", file); + : Logger.log("session skipped", file); } } }); diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js index b940eab..2236cff 100644 --- a/websvr/SessionParser.js +++ b/websvr/SessionParser.js @@ -72,7 +72,7 @@ SessionParser.prototype = { //Write or modify json file fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { if (err) { - console.log(err); + Logger.debug(err); return; } @@ -112,7 +112,7 @@ SessionParser.prototype = { } else if (SessionManager.isValid(self.sid)) { fs.readFile(sessionPath, function(err, data) { if (err) { - console.log(err); + Logger.debug(err); return; }; data = data || "{}"; diff --git a/websvr/Settings.js b/websvr/Settings.js index b261824..d60b659 100644 --- a/websvr/Settings.js +++ b/websvr/Settings.js @@ -16,7 +16,7 @@ var Settings = { //enable client-side cache(304)? cache: true, //enable debug information output - debug: false, + debug: true, //receive buffer, default size 32k, i.e.: receive post data from ajax request bufferSize: 32768, diff --git a/websvr/Template.js b/websvr/Template.js index b662842..5d89cd9 100644 --- a/websvr/Template.js +++ b/websvr/Template.js @@ -10,7 +10,7 @@ var Template = (function() { var fullpath = path.join(Settings.root, filename); fs.readFile(fullpath, function (err, html) { - err && console.log(err); + err && Logger.debug(err); err ? cb("") : cb(html); }); }; @@ -21,7 +21,7 @@ var Template = (function() { tmplFn = engine.compile(chrunk, params); outFn(tmplFn(params)); } catch(err) { - console.log(err); + Logger.debug(err); outFn(err); } }; diff --git a/websvr/test/lib/MIME.types b/websvr/test/lib/MIME.types new file mode 100644 index 0000000..739a544 --- /dev/null +++ b/websvr/test/lib/MIME.types @@ -0,0 +1,105 @@ +var MIMETYPES = module.exports = { + "js": "application/javascript", + "json": "application/json", + "doc": "application/msword", + "pdf": "application/pdf", + "7z": "application/x-7z-compressed", + "torrent": "application/x-bittorrent", + "bz": "application/x-bzip", + "bz2": "application/x-bzip2", + "boz": "application/x-bzip2", + "application": "application/x-ms-application", + "rar": "application/x-rar-compressed", + "sh": "application/x-sh", + "swf": "application/x-shockwave-flash", + "xap": "application/x-silverlight-app", + "tar": "application/x-tar", + "der": "application/x-x509-ca-cert", + "crt": "application/x-x509-ca-cert", + "xhtml": "application/xhtml+xml", + "xht": "application/xhtml+xml", + "xml": "application/xml", + "xsl": "application/xml", + "dtd": "application/xml-dtd", + "xop": "application/xop+xml", + "xslt": "application/xslt+xml", + "xspf": "application/xspf+xml", + "zip": "application/zip", + "adp": "audio/adpcm", + "au": "audio/basic", + "snd": "audio/basic", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "rmi": "audio/midi", + "mp4a": "audio/mp4", + "mpga": "audio/mpeg", + "mp2": "audio/mpeg", + "mp2a": "audio/mpeg", + "mp3": "audio/mpeg", + "m2a": "audio/mpeg", + "m3a": "audio/mpeg", + "oga": "audio/ogg", + "ogg": "audio/ogg", + "weba": "audio/webm", + "wav": "audio/x-wav", + "bmp": "image/bmp", + "cgm": "image/cgm", + "gif": "image/gif", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "png": "image/png", + "btif": "image/prs.btif", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "tiff": "image/tiff", + "tif": "image/tiff", + "psd": "image/vnd.adobe.photoshop", + "mdi": "image/vnd.ms-modi", + "ico": "image/x-icon", + "eml": "message/rfc822", + "mime": "message/rfc822", + "css": "text/css", + "csv": "text/csv", + "html": "text/html", + "htm": "text/html", + "txt": "text/plain", + "text": "text/plain", + "conf": "text/plain", + "def": "text/plain", + "list": "text/plain", + "log": "text/plain", + "wml": "text/vnd.wap.wml", + "wmls": "text/vnd.wap.wmlscript", + "3gp": "video/3gpp", + "3g2": "video/3gpp2", + "h261": "video/h261", + "h263": "video/h263", + "h264": "video/h264", + "jpgv": "video/jpeg", + "jpm": "video/jpm", + "jpgm": "video/jpm", + "mp4": "video/mp4", + "mp4v": "video/mp4", + "mpg4": "video/mp4", + "mpeg": "video/mpeg", + "mpg": "video/mpeg", + "mpe": "video/mpeg", + "m1v": "video/mpeg", + "m2v": "video/mpeg", + "ogv": "video/ogg", + "qt": "video/quicktime", + "mov": "video/quicktime", + "webm": "video/webm", + "f4v": "video/x-f4v", + "fli": "video/x-fli", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "asf": "video/x-ms-asf", + "asx": "video/x-ms-asf", + "wm": "video/x-ms-wm", + "wmv": "video/x-ms-wmv", + "wmx": "video/x-ms-wmx", + "wvx": "video/x-ms-wvx", + "avi": "video/x-msvideo", +}; \ No newline at end of file diff --git a/websvr/test/lib/MIMETYPES.js b/websvr/test/lib/MIMETYPES.js new file mode 100644 index 0000000..85422a6 --- /dev/null +++ b/websvr/test/lib/MIMETYPES.js @@ -0,0 +1,901 @@ +var MIMETYPES = module.exports = { + "123": "application/vnd.lotus-1-2-3", + "ez": "application/andrew-inset", + "aw": "application/applixware", + "atom": "application/atom+xml", + "atomcat": "application/atomcat+xml", + "atomsvc": "application/atomsvc+xml", + "ccxml": "application/ccxml+xml", + "cdmia": "application/cdmi-capability", + "cdmic": "application/cdmi-container", + "cdmid": "application/cdmi-domain", + "cdmio": "application/cdmi-object", + "cdmiq": "application/cdmi-queue", + "cu": "application/cu-seeme", + "davmount": "application/davmount+xml", + "dssc": "application/dssc+der", + "xdssc": "application/dssc+xml", + "ecma": "application/ecmascript", + "emma": "application/emma+xml", + "epub": "application/epub+zip", + "exi": "application/exi", + "pfr": "application/font-tdpfr", + "stk": "application/hyperstudio", + "ink": "application/inkml+xml", + "inkml": "application/inkml+xml", + "ipfix": "application/ipfix", + "jar": "application/java-archive", + "ser": "application/java-serialized-object", + "class": "application/java-vm", + "js": "application/javascript", + "json": "application/json", + "lostxml": "application/lost+xml", + "hqx": "application/mac-binhex40", + "cpt": "application/mac-compactpro", + "mads": "application/mads+xml", + "mrc": "application/marc", + "mrcx": "application/marcxml+xml", + "ma": "application/mathematica", + "nb": "application/mathematica", + "mb": "application/mathematica", + "mathml": "application/mathml+xml", + "mbox": "application/mbox", + "mscml": "application/mediaservercontrol+xml", + "meta4": "application/metalink4+xml", + "mets": "application/mets+xml", + "mods": "application/mods+xml", + "m21": "application/mp21", + "mp21": "application/mp21", + "mp4s": "application/mp4", + "doc": "application/msword", + "dot": "application/msword", + "mxf": "application/mxf", + "bin": "application/octet-stream", + "dms": "application/octet-stream", + "lha": "application/octet-stream", + "lrf": "application/octet-stream", + "lzh": "application/octet-stream", + "so": "application/octet-stream", + "iso": "application/octet-stream", + "dmg": "application/octet-stream", + "dist": "application/octet-stream", + "distz": "application/octet-stream", + "pkg": "application/octet-stream", + "bpk": "application/octet-stream", + "dump": "application/octet-stream", + "elc": "application/octet-stream", + "deploy": "application/octet-stream", + "oda": "application/oda", + "opf": "application/oebps-package+xml", + "ogx": "application/ogg", + "onetoc": "application/onenote", + "onetoc2": "application/onenote", + "onetmp": "application/onenote", + "onepkg": "application/onenote", + "oxps": "application/oxps", + "xer": "application/patch-ops-error+xml", + "pdf": "application/pdf", + "pgp": "application/pgp-encrypted", + "asc": "application/pgp-signature", + "sig": "application/pgp-signature", + "prf": "application/pics-rules", + "p10": "application/pkcs10", + "p7m": "application/pkcs7-mime", + "p7c": "application/pkcs7-mime", + "p7s": "application/pkcs7-signature", + "p8": "application/pkcs8", + "ac": "application/pkix-attr-cert", + "cer": "application/pkix-cert", + "crl": "application/pkix-crl", + "pkipath": "application/pkix-pkipath", + "pki": "application/pkixcmp", + "pls": "application/pls+xml", + "ai": "application/postscript", + "eps": "application/postscript", + "ps": "application/postscript", + "cww": "application/prs.cww", + "pskcxml": "application/pskc+xml", + "rdf": "application/rdf+xml", + "rif": "application/reginfo+xml", + "rnc": "application/relax-ng-compact-syntax", + "rl": "application/resource-lists+xml", + "rld": "application/resource-lists-diff+xml", + "rs": "application/rls-services+xml", + "gbr": "application/rpki-ghostbusters", + "mft": "application/rpki-manifest", + "roa": "application/rpki-roa", + "rsd": "application/rsd+xml", + "rss": "application/rss+xml", + "rtf": "application/rtf", + "sbml": "application/sbml+xml", + "scq": "application/scvp-cv-request", + "scs": "application/scvp-cv-response", + "spq": "application/scvp-vp-request", + "spp": "application/scvp-vp-response", + "sdp": "application/sdp", + "setpay": "application/set-payment-initiation", + "setreg": "application/set-registration-initiation", + "shf": "application/shf+xml", + "smi": "application/smil+xml", + "smil": "application/smil+xml", + "rq": "application/sparql-query", + "srx": "application/sparql-results+xml", + "gram": "application/srgs", + "grxml": "application/srgs+xml", + "sru": "application/sru+xml", + "ssml": "application/ssml+xml", + "tei": "application/tei+xml", + "teicorpus": "application/tei+xml", + "tfi": "application/thraud+xml", + "tsd": "application/timestamped-data", + "plb": "application/vnd.3gpp.pic-bw-large", + "psb": "application/vnd.3gpp.pic-bw-small", + "pvb": "application/vnd.3gpp.pic-bw-var", + "tcap": "application/vnd.3gpp2.tcap", + "pwn": "application/vnd.3m.post-it-notes", + "aso": "application/vnd.accpac.simply.aso", + "imp": "application/vnd.accpac.simply.imp", + "acu": "application/vnd.acucobol", + "atc": "application/vnd.acucorp", + "acutc": "application/vnd.acucorp", + "air": "application/vnd.adobe.air-application-installer-package+zip", + "fxp": "application/vnd.adobe.fxp", + "fxpl": "application/vnd.adobe.fxp", + "xdp": "application/vnd.adobe.xdp+xml", + "xfdf": "application/vnd.adobe.xfdf", + "ahead": "application/vnd.ahead.space", + "azf": "application/vnd.airzip.filesecure.azf", + "azs": "application/vnd.airzip.filesecure.azs", + "azw": "application/vnd.amazon.ebook", + "acc": "application/vnd.americandynamics.acc", + "ami": "application/vnd.amiga.ami", + "apk": "application/vnd.android.package-archive", + "cii": "application/vnd.anser-web-certificate-issue-initiation", + "fti": "application/vnd.anser-web-funds-transfer-initiation", + "atx": "application/vnd.antix.game-component", + "mpkg": "application/vnd.apple.installer+xml", + "m3u8": "application/vnd.apple.mpegurl", + "swi": "application/vnd.aristanetworks.swi", + "iota": "application/vnd.astraea-software.iota", + "aep": "application/vnd.audiograph", + "mpm": "application/vnd.blueice.multipass", + "bmi": "application/vnd.bmi", + "rep": "application/vnd.businessobjects", + "cdxml": "application/vnd.chemdraw+xml", + "mmd": "application/vnd.chipnuts.karaoke-mmd", + "cdy": "application/vnd.cinderella", + "cla": "application/vnd.claymore", + "rp9": "application/vnd.cloanto.rp9", + "c4g": "application/vnd.clonk.c4group", + "c4d": "application/vnd.clonk.c4group", + "c4f": "application/vnd.clonk.c4group", + "c4p": "application/vnd.clonk.c4group", + "c4u": "application/vnd.clonk.c4group", + "c11amc": "application/vnd.cluetrust.cartomobile-config", + "c11amz": "application/vnd.cluetrust.cartomobile-config-pkg", + "csp": "application/vnd.commonspace", + "cdbcmsg": "application/vnd.contact.cmsg", + "cmc": "application/vnd.cosmocaller", + "clkx": "application/vnd.crick.clicker", + "clkk": "application/vnd.crick.clicker.keyboard", + "clkp": "application/vnd.crick.clicker.palette", + "clkt": "application/vnd.crick.clicker.template", + "clkw": "application/vnd.crick.clicker.wordbank", + "wbs": "application/vnd.criticaltools.wbs+xml", + "pml": "application/vnd.ctc-posml", + "ppd": "application/vnd.cups-ppd", + "car": "application/vnd.curl.car", + "pcurl": "application/vnd.curl.pcurl", + "rdz": "application/vnd.data-vision.rdz", + "uvf": "application/vnd.dece.data", + "uvvf": "application/vnd.dece.data", + "uvd": "application/vnd.dece.data", + "uvvd": "application/vnd.dece.data", + "uvt": "application/vnd.dece.ttml+xml", + "uvvt": "application/vnd.dece.ttml+xml", + "uvx": "application/vnd.dece.unspecified", + "uvvx": "application/vnd.dece.unspecified", + "uvz": "application/vnd.dece.zip", + "uvvz": "application/vnd.dece.zip", + "fe_launch": "application/vnd.denovo.fcselayout-link", + "dna": "application/vnd.dna", + "mlp": "application/vnd.dolby.mlp", + "dpg": "application/vnd.dpgraph", + "dfac": "application/vnd.dreamfactory", + "ait": "application/vnd.dvb.ait", + "svc": "application/vnd.dvb.service", + "geo": "application/vnd.dynageo", + "mag": "application/vnd.ecowin.chart", + "nml": "application/vnd.enliven", + "esf": "application/vnd.epson.esf", + "msf": "application/vnd.epson.msf", + "qam": "application/vnd.epson.quickanime", + "slt": "application/vnd.epson.salt", + "ssf": "application/vnd.epson.ssf", + "es3": "application/vnd.eszigno3+xml", + "et3": "application/vnd.eszigno3+xml", + "ez2": "application/vnd.ezpix-album", + "ez3": "application/vnd.ezpix-package", + "fdf": "application/vnd.fdf", + "mseed": "application/vnd.fdsn.mseed", + "seed": "application/vnd.fdsn.seed", + "dataless": "application/vnd.fdsn.seed", + "gph": "application/vnd.flographit", + "ftc": "application/vnd.fluxtime.clip", + "fm": "application/vnd.framemaker", + "frame": "application/vnd.framemaker", + "maker": "application/vnd.framemaker", + "book": "application/vnd.framemaker", + "fnc": "application/vnd.frogans.fnc", + "ltf": "application/vnd.frogans.ltf", + "fsc": "application/vnd.fsc.weblaunch", + "oas": "application/vnd.fujitsu.oasys", + "oa2": "application/vnd.fujitsu.oasys2", + "oa3": "application/vnd.fujitsu.oasys3", + "fg5": "application/vnd.fujitsu.oasysgp", + "bh2": "application/vnd.fujitsu.oasysprs", + "ddd": "application/vnd.fujixerox.ddd", + "xdw": "application/vnd.fujixerox.docuworks", + "xbd": "application/vnd.fujixerox.docuworks.binder", + "fzs": "application/vnd.fuzzysheet", + "txd": "application/vnd.genomatix.tuxedo", + "ggb": "application/vnd.geogebra.file", + "ggt": "application/vnd.geogebra.tool", + "gex": "application/vnd.geometry-explorer", + "gre": "application/vnd.geometry-explorer", + "gxt": "application/vnd.geonext", + "g2w": "application/vnd.geoplan", + "g3w": "application/vnd.geospace", + "gmx": "application/vnd.gmx", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "gqf": "application/vnd.grafeq", + "gqs": "application/vnd.grafeq", + "gac": "application/vnd.groove-account", + "ghf": "application/vnd.groove-help", + "gim": "application/vnd.groove-identity-message", + "grv": "application/vnd.groove-injector", + "gtm": "application/vnd.groove-tool-message", + "tpl": "application/vnd.groove-tool-template", + "vcg": "application/vnd.groove-vcard", + "hal": "application/vnd.hal+xml", + "zmm": "application/vnd.handheld-entertainment+xml", + "hbci": "application/vnd.hbci", + "les": "application/vnd.hhe.lesson-player", + "hpgl": "application/vnd.hp-hpgl", + "hpid": "application/vnd.hp-hpid", + "hps": "application/vnd.hp-hps", + "jlt": "application/vnd.hp-jlyt", + "pcl": "application/vnd.hp-pcl", + "pclxl": "application/vnd.hp-pclxl", + "sfd-hdstx": "application/vnd.hydrostatix.sof-data", + "x3d": "application/vnd.hzn-3d-crossword", + "mpy": "application/vnd.ibm.minipay", + "afp": "application/vnd.ibm.modcap", + "listafp": "application/vnd.ibm.modcap", + "list3820": "application/vnd.ibm.modcap", + "irm": "application/vnd.ibm.rights-management", + "sc": "application/vnd.ibm.secure-container", + "icc": "application/vnd.iccprofile", + "icm": "application/vnd.iccprofile", + "igl": "application/vnd.igloader", + "ivp": "application/vnd.immervision-ivp", + "ivu": "application/vnd.immervision-ivu", + "igm": "application/vnd.insors.igm", + "xpw": "application/vnd.intercon.formnet", + "xpx": "application/vnd.intercon.formnet", + "i2g": "application/vnd.intergeo", + "qbo": "application/vnd.intu.qbo", + "qfx": "application/vnd.intu.qfx", + "rcprofile": "application/vnd.ipunplugged.rcprofile", + "irp": "application/vnd.irepository.package+xml", + "xpr": "application/vnd.is-xpr", + "fcs": "application/vnd.isac.fcs", + "jam": "application/vnd.jam", + "rms": "application/vnd.jcp.javame.midlet-rms", + "jisp": "application/vnd.jisp", + "joda": "application/vnd.joost.joda-archive", + "ktz": "application/vnd.kahootz", + "ktr": "application/vnd.kahootz", + "karbon": "application/vnd.kde.karbon", + "chrt": "application/vnd.kde.kchart", + "kfo": "application/vnd.kde.kformula", + "flw": "application/vnd.kde.kivio", + "kon": "application/vnd.kde.kontour", + "kpr": "application/vnd.kde.kpresenter", + "kpt": "application/vnd.kde.kpresenter", + "ksp": "application/vnd.kde.kspread", + "kwd": "application/vnd.kde.kword", + "kwt": "application/vnd.kde.kword", + "htke": "application/vnd.kenameaapp", + "kia": "application/vnd.kidspiration", + "kne": "application/vnd.kinar", + "knp": "application/vnd.kinar", + "skp": "application/vnd.koan", + "skd": "application/vnd.koan", + "skt": "application/vnd.koan", + "skm": "application/vnd.koan", + "sse": "application/vnd.kodak-descriptor", + "lasxml": "application/vnd.las.las+xml", + "lbd": "application/vnd.llamagraphics.life-balance.desktop", + "lbe": "application/vnd.llamagraphics.life-balance.exchange+xml", + "apr": "application/vnd.lotus-approach", + "pre": "application/vnd.lotus-freelance", + "nsf": "application/vnd.lotus-notes", + "org": "application/vnd.lotus-organizer", + "scm": "application/vnd.lotus-screencam", + "lwp": "application/vnd.lotus-wordpro", + "portpkg": "application/vnd.macports.portpkg", + "mcd": "application/vnd.mcd", + "mc1": "application/vnd.medcalcdata", + "cdkey": "application/vnd.mediastation.cdkey", + "mwf": "application/vnd.mfer", + "mfm": "application/vnd.mfmp", + "flo": "application/vnd.micrografx.flo", + "igx": "application/vnd.micrografx.igx", + "mif": "application/vnd.mif", + "daf": "application/vnd.mobius.daf", + "dis": "application/vnd.mobius.dis", + "mbk": "application/vnd.mobius.mbk", + "mqy": "application/vnd.mobius.mqy", + "msl": "application/vnd.mobius.msl", + "plc": "application/vnd.mobius.plc", + "txf": "application/vnd.mobius.txf", + "mpn": "application/vnd.mophun.application", + "mpc": "application/vnd.mophun.certificate", + "xul": "application/vnd.mozilla.xul+xml", + "cil": "application/vnd.ms-artgalry", + "cab": "application/vnd.ms-cab-compressed", + "xls": "application/vnd.ms-excel", + "xlm": "application/vnd.ms-excel", + "xla": "application/vnd.ms-excel", + "xlc": "application/vnd.ms-excel", + "xlt": "application/vnd.ms-excel", + "xlw": "application/vnd.ms-excel", + "xlam": "application/vnd.ms-excel.addin.macroenabled.12", + "xlsb": "application/vnd.ms-excel.sheet.binary.macroenabled.12", + "xlsm": "application/vnd.ms-excel.sheet.macroenabled.12", + "xltm": "application/vnd.ms-excel.template.macroenabled.12", + "eot": "application/vnd.ms-fontobject", + "chm": "application/vnd.ms-htmlhelp", + "ims": "application/vnd.ms-ims", + "lrm": "application/vnd.ms-lrm", + "thmx": "application/vnd.ms-officetheme", + "cat": "application/vnd.ms-pki.seccat", + "stl": "application/vnd.ms-pki.stl", + "ppt": "application/vnd.ms-powerpoint", + "pps": "application/vnd.ms-powerpoint", + "pot": "application/vnd.ms-powerpoint", + "ppam": "application/vnd.ms-powerpoint.addin.macroenabled.12", + "pptm": "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "sldm": "application/vnd.ms-powerpoint.slide.macroenabled.12", + "ppsm": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", + "potm": "application/vnd.ms-powerpoint.template.macroenabled.12", + "mpp": "application/vnd.ms-project", + "mpt": "application/vnd.ms-project", + "docm": "application/vnd.ms-word.document.macroenabled.12", + "dotm": "application/vnd.ms-word.template.macroenabled.12", + "wps": "application/vnd.ms-works", + "wks": "application/vnd.ms-works", + "wcm": "application/vnd.ms-works", + "wdb": "application/vnd.ms-works", + "wpl": "application/vnd.ms-wpl", + "xps": "application/vnd.ms-xpsdocument", + "mseq": "application/vnd.mseq", + "mus": "application/vnd.musician", + "msty": "application/vnd.muvee.style", + "taglet": "application/vnd.mynfc", + "nlu": "application/vnd.neurolanguage.nlu", + "nnd": "application/vnd.noblenet-directory", + "nns": "application/vnd.noblenet-sealer", + "nnw": "application/vnd.noblenet-web", + "ngdat": "application/vnd.nokia.n-gage.data", + "n-gage": "application/vnd.nokia.n-gage.symbian.install", + "rpst": "application/vnd.nokia.radio-preset", + "rpss": "application/vnd.nokia.radio-presets", + "edm": "application/vnd.novadigm.edm", + "edx": "application/vnd.novadigm.edx", + "ext": "application/vnd.novadigm.ext", + "odc": "application/vnd.oasis.opendocument.chart", + "otc": "application/vnd.oasis.opendocument.chart-template", + "odb": "application/vnd.oasis.opendocument.database", + "odf": "application/vnd.oasis.opendocument.formula", + "odft": "application/vnd.oasis.opendocument.formula-template", + "odg": "application/vnd.oasis.opendocument.graphics", + "otg": "application/vnd.oasis.opendocument.graphics-template", + "odi": "application/vnd.oasis.opendocument.image", + "oti": "application/vnd.oasis.opendocument.image-template", + "odp": "application/vnd.oasis.opendocument.presentation", + "otp": "application/vnd.oasis.opendocument.presentation-template", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "ots": "application/vnd.oasis.opendocument.spreadsheet-template", + "odt": "application/vnd.oasis.opendocument.text", + "odm": "application/vnd.oasis.opendocument.text-master", + "ott": "application/vnd.oasis.opendocument.text-template", + "oth": "application/vnd.oasis.opendocument.text-web", + "xo": "application/vnd.olpc-sugar", + "dd2": "application/vnd.oma.dd2+xml", + "oxt": "application/vnd.openofficeorg.extension", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", + "ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "potx": "application/vnd.openxmlformats-officedocument.presentationml.template", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "mgp": "application/vnd.osgeo.mapguide.package", + "dp": "application/vnd.osgi.dp", + "pdb": "application/vnd.palm", + "pqa": "application/vnd.palm", + "oprc": "application/vnd.palm", + "paw": "application/vnd.pawaafile", + "str": "application/vnd.pg.format", + "ei6": "application/vnd.pg.osasli", + "efif": "application/vnd.picsel", + "wg": "application/vnd.pmi.widget", + "plf": "application/vnd.pocketlearn", + "pbd": "application/vnd.powerbuilder6", + "box": "application/vnd.previewsystems.box", + "mgz": "application/vnd.proteus.magazine", + "qps": "application/vnd.publishare-delta-tree", + "ptid": "application/vnd.pvi.ptid1", + "qxd": "application/vnd.quark.quarkxpress", + "qxt": "application/vnd.quark.quarkxpress", + "qwd": "application/vnd.quark.quarkxpress", + "qwt": "application/vnd.quark.quarkxpress", + "qxl": "application/vnd.quark.quarkxpress", + "qxb": "application/vnd.quark.quarkxpress", + "bed": "application/vnd.realvnc.bed", + "mxl": "application/vnd.recordare.musicxml", + "musicxml": "application/vnd.recordare.musicxml+xml", + "cryptonote": "application/vnd.rig.cryptonote", + "cod": "application/vnd.rim.cod", + "rm": "application/vnd.rn-realmedia", + "link66": "application/vnd.route66.link66+xml", + "st": "application/vnd.sailingtracker.track", + "see": "application/vnd.seemail", + "sema": "application/vnd.sema", + "semd": "application/vnd.semd", + "semf": "application/vnd.semf", + "ifm": "application/vnd.shana.informed.formdata", + "itp": "application/vnd.shana.informed.formtemplate", + "iif": "application/vnd.shana.informed.interchange", + "ipk": "application/vnd.shana.informed.package", + "twd": "application/vnd.simtech-mindmapper", + "twds": "application/vnd.simtech-mindmapper", + "mmf": "application/vnd.smaf", + "teacher": "application/vnd.smart.teacher", + "sdkm": "application/vnd.solent.sdkm+xml", + "sdkd": "application/vnd.solent.sdkm+xml", + "dxp": "application/vnd.spotfire.dxp", + "sfs": "application/vnd.spotfire.sfs", + "sdc": "application/vnd.stardivision.calc", + "sda": "application/vnd.stardivision.draw", + "sdd": "application/vnd.stardivision.impress", + "smf": "application/vnd.stardivision.math", + "sdw": "application/vnd.stardivision.writer", + "vor": "application/vnd.stardivision.writer", + "sgl": "application/vnd.stardivision.writer-global", + "smzip": "application/vnd.stepmania.package", + "sm": "application/vnd.stepmania.stepchart", + "sxc": "application/vnd.sun.xml.calc", + "stc": "application/vnd.sun.xml.calc.template", + "sxd": "application/vnd.sun.xml.draw", + "std": "application/vnd.sun.xml.draw.template", + "sxi": "application/vnd.sun.xml.impress", + "sti": "application/vnd.sun.xml.impress.template", + "sxm": "application/vnd.sun.xml.math", + "sxw": "application/vnd.sun.xml.writer", + "sxg": "application/vnd.sun.xml.writer.global", + "stw": "application/vnd.sun.xml.writer.template", + "sus": "application/vnd.sus-calendar", + "susp": "application/vnd.sus-calendar", + "svd": "application/vnd.svd", + "sis": "application/vnd.symbian.install", + "sisx": "application/vnd.symbian.install", + "xsm": "application/vnd.syncml+xml", + "bdm": "application/vnd.syncml.dm+wbxml", + "xdm": "application/vnd.syncml.dm+xml", + "tao": "application/vnd.tao.intent-module-archive", + "pcap": "application/vnd.tcpdump.pcap", + "cap": "application/vnd.tcpdump.pcap", + "dmp": "application/vnd.tcpdump.pcap", + "tmo": "application/vnd.tmobile-livetv", + "tpt": "application/vnd.trid.tpt", + "mxs": "application/vnd.triscape.mxs", + "tra": "application/vnd.trueapp", + "ufd": "application/vnd.ufdl", + "ufdl": "application/vnd.ufdl", + "utz": "application/vnd.uiq.theme", + "umj": "application/vnd.umajin", + "unityweb": "application/vnd.unity", + "uoml": "application/vnd.uoml+xml", + "vcx": "application/vnd.vcx", + "vsd": "application/vnd.visio", + "vst": "application/vnd.visio", + "vss": "application/vnd.visio", + "vsw": "application/vnd.visio", + "vis": "application/vnd.visionary", + "vsf": "application/vnd.vsf", + "wbxml": "application/vnd.wap.wbxml", + "wmlc": "application/vnd.wap.wmlc", + "wmlsc": "application/vnd.wap.wmlscriptc", + "wtb": "application/vnd.webturbo", + "nbp": "application/vnd.wolfram.player", + "wpd": "application/vnd.wordperfect", + "wqd": "application/vnd.wqd", + "stf": "application/vnd.wt.stf", + "xar": "application/vnd.xara", + "xfdl": "application/vnd.xfdl", + "hvd": "application/vnd.yamaha.hv-dic", + "hvs": "application/vnd.yamaha.hv-script", + "hvp": "application/vnd.yamaha.hv-voice", + "osf": "application/vnd.yamaha.openscoreformat", + "osfpvg": "application/vnd.yamaha.openscoreformat.osfpvg+xml", + "saf": "application/vnd.yamaha.smaf-audio", + "spf": "application/vnd.yamaha.smaf-phrase", + "cmp": "application/vnd.yellowriver-custom-menu", + "zir": "application/vnd.zul", + "zirz": "application/vnd.zul", + "zaz": "application/vnd.zzazz.deck+xml", + "vxml": "application/voicexml+xml", + "wgt": "application/widget", + "hlp": "application/winhlp", + "wsdl": "application/wsdl+xml", + "wspolicy": "application/wspolicy+xml", + "7z": "application/x-7z-compressed", + "abw": "application/x-abiword", + "ace": "application/x-ace-compressed", + "aab": "application/x-authorware-bin", + "x32": "application/x-authorware-bin", + "u32": "application/x-authorware-bin", + "vox": "application/x-authorware-bin", + "aam": "application/x-authorware-map", + "aas": "application/x-authorware-seg", + "bcpio": "application/x-bcpio", + "torrent": "application/x-bittorrent", + "bz": "application/x-bzip", + "bz2": "application/x-bzip2", + "boz": "application/x-bzip2", + "vcd": "application/x-cdlink", + "chat": "application/x-chat", + "pgn": "application/x-chess-pgn", + "cpio": "application/x-cpio", + "csh": "application/x-csh", + "deb": "application/x-debian-package", + "udeb": "application/x-debian-package", + "dir": "application/x-director", + "dcr": "application/x-director", + "dxr": "application/x-director", + "cst": "application/x-director", + "cct": "application/x-director", + "cxt": "application/x-director", + "w3d": "application/x-director", + "fgd": "application/x-director", + "swa": "application/x-director", + "wad": "application/x-doom", + "ncx": "application/x-dtbncx+xml", + "dtb": "application/x-dtbook+xml", + "res": "application/x-dtbresource+xml", + "dvi": "application/x-dvi", + "bdf": "application/x-font-bdf", + "gsf": "application/x-font-ghostscript", + "psf": "application/x-font-linux-psf", + "otf": "application/x-font-otf", + "pcf": "application/x-font-pcf", + "snf": "application/x-font-snf", + "ttf": "application/x-font-ttf", + "ttc": "application/x-font-ttf", + "pfa": "application/x-font-type1", + "pfb": "application/x-font-type1", + "pfm": "application/x-font-type1", + "afm": "application/x-font-type1", + "woff": "application/x-font-woff", + "spl": "application/x-futuresplash", + "gnumeric": "application/x-gnumeric", + "gtar": "application/x-gtar", + "hdf": "application/x-hdf", + "jnlp": "application/x-java-jnlp-file", + "latex": "application/x-latex", + "prc": "application/x-mobipocket-ebook", + "mobi": "application/x-mobipocket-ebook", + "application": "application/x-ms-application", + "wmd": "application/x-ms-wmd", + "wmz": "application/x-ms-wmz", + "xbap": "application/x-ms-xbap", + "mdb": "application/x-msaccess", + "obd": "application/x-msbinder", + "crd": "application/x-mscardfile", + "clp": "application/x-msclip", + "exe": "application/x-msdownload", + "dll": "application/x-msdownload", + "com": "application/x-msdownload", + "bat": "application/x-msdownload", + "msi": "application/x-msdownload", + "mvb": "application/x-msmediaview", + "m13": "application/x-msmediaview", + "m14": "application/x-msmediaview", + "wmf": "application/x-msmetafile", + "mny": "application/x-msmoney", + "pub": "application/x-mspublisher", + "scd": "application/x-msschedule", + "trm": "application/x-msterminal", + "wri": "application/x-mswrite", + "nc": "application/x-netcdf", + "cdf": "application/x-netcdf", + "p12": "application/x-pkcs12", + "pfx": "application/x-pkcs12", + "p7b": "application/x-pkcs7-certificates", + "spc": "application/x-pkcs7-certificates", + "p7r": "application/x-pkcs7-certreqresp", + "rar": "application/x-rar-compressed", + "sh": "application/x-sh", + "shar": "application/x-shar", + "swf": "application/x-shockwave-flash", + "xap": "application/x-silverlight-app", + "sit": "application/x-stuffit", + "sitx": "application/x-stuffitx", + "sv4cpio": "application/x-sv4cpio", + "sv4crc": "application/x-sv4crc", + "tar": "application/x-tar", + "tcl": "application/x-tcl", + "tex": "application/x-tex", + "tfm": "application/x-tex-tfm", + "texinfo": "application/x-texinfo", + "texi": "application/x-texinfo", + "ustar": "application/x-ustar", + "src": "application/x-wais-source", + "der": "application/x-x509-ca-cert", + "crt": "application/x-x509-ca-cert", + "fig": "application/x-xfig", + "xpi": "application/x-xpinstall", + "xdf": "application/xcap-diff+xml", + "xenc": "application/xenc+xml", + "xhtml": "application/xhtml+xml", + "xht": "application/xhtml+xml", + "xml": "application/xml", + "xsl": "application/xml", + "dtd": "application/xml-dtd", + "xop": "application/xop+xml", + "xslt": "application/xslt+xml", + "xspf": "application/xspf+xml", + "mxml": "application/xv+xml", + "xhvml": "application/xv+xml", + "xvml": "application/xv+xml", + "xvm": "application/xv+xml", + "yang": "application/yang", + "yin": "application/yin+xml", + "zip": "application/zip", + "adp": "audio/adpcm", + "au": "audio/basic", + "snd": "audio/basic", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "rmi": "audio/midi", + "mp4a": "audio/mp4", + "mpga": "audio/mpeg", + "mp2": "audio/mpeg", + "mp2a": "audio/mpeg", + "mp3": "audio/mpeg", + "m2a": "audio/mpeg", + "m3a": "audio/mpeg", + "oga": "audio/ogg", + "ogg": "audio/ogg", + "spx": "audio/ogg", + "uva": "audio/vnd.dece.audio", + "uvva": "audio/vnd.dece.audio", + "eol": "audio/vnd.digital-winds", + "dra": "audio/vnd.dra", + "dts": "audio/vnd.dts", + "dtshd": "audio/vnd.dts.hd", + "lvp": "audio/vnd.lucent.voice", + "pya": "audio/vnd.ms-playready.media.pya", + "ecelp4800": "audio/vnd.nuera.ecelp4800", + "ecelp7470": "audio/vnd.nuera.ecelp7470", + "ecelp9600": "audio/vnd.nuera.ecelp9600", + "rip": "audio/vnd.rip", + "weba": "audio/webm", + "aac": "audio/x-aac", + "aif": "audio/x-aiff", + "aiff": "audio/x-aiff", + "aifc": "audio/x-aiff", + "m3u": "audio/x-mpegurl", + "wax": "audio/x-ms-wax", + "wma": "audio/x-ms-wma", + "ram": "audio/x-pn-realaudio", + "ra": "audio/x-pn-realaudio", + "rmp": "audio/x-pn-realaudio-plugin", + "wav": "audio/x-wav", + "cdx": "chemical/x-cdx", + "cif": "chemical/x-cif", + "cmdf": "chemical/x-cmdf", + "cml": "chemical/x-cml", + "csml": "chemical/x-csml", + "xyz": "chemical/x-xyz", + "bmp": "image/bmp", + "cgm": "image/cgm", + "g3": "image/g3fax", + "gif": "image/gif", + "ief": "image/ief", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "jpe": "image/jpeg", + "ktx": "image/ktx", + "png": "image/png", + "btif": "image/prs.btif", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "tiff": "image/tiff", + "tif": "image/tiff", + "psd": "image/vnd.adobe.photoshop", + "uvi": "image/vnd.dece.graphic", + "uvvi": "image/vnd.dece.graphic", + "uvg": "image/vnd.dece.graphic", + "uvvg": "image/vnd.dece.graphic", + "sub": "text/vnd.dvb.subtitle", + "djvu": "image/vnd.djvu", + "djv": "image/vnd.djvu", + "dwg": "image/vnd.dwg", + "dxf": "image/vnd.dxf", + "fbs": "image/vnd.fastbidsheet", + "fpx": "image/vnd.fpx", + "fst": "image/vnd.fst", + "mmr": "image/vnd.fujixerox.edmics-mmr", + "rlc": "image/vnd.fujixerox.edmics-rlc", + "mdi": "image/vnd.ms-modi", + "npx": "image/vnd.net-fpx", + "wbmp": "image/vnd.wap.wbmp", + "xif": "image/vnd.xiff", + "webp": "image/webp", + "ras": "image/x-cmu-raster", + "cmx": "image/x-cmx", + "fh": "image/x-freehand", + "fhc": "image/x-freehand", + "fh4": "image/x-freehand", + "fh5": "image/x-freehand", + "fh7": "image/x-freehand", + "ico": "image/x-icon", + "pcx": "image/x-pcx", + "pic": "image/x-pict", + "pct": "image/x-pict", + "pnm": "image/x-portable-anymap", + "pbm": "image/x-portable-bitmap", + "pgm": "image/x-portable-graymap", + "ppm": "image/x-portable-pixmap", + "rgb": "image/x-rgb", + "xbm": "image/x-xbitmap", + "xpm": "image/x-xpixmap", + "xwd": "image/x-xwindowdump", + "eml": "message/rfc822", + "mime": "message/rfc822", + "igs": "model/iges", + "iges": "model/iges", + "msh": "model/mesh", + "mesh": "model/mesh", + "silo": "model/mesh", + "dae": "model/vnd.collada+xml", + "dwf": "model/vnd.dwf", + "gdl": "model/vnd.gdl", + "gtw": "model/vnd.gtw", + "mts": "model/vnd.mts", + "vtu": "model/vnd.vtu", + "wrl": "model/vrml", + "vrml": "model/vrml", + "ics": "text/calendar", + "ifb": "text/calendar", + "css": "text/css", + "csv": "text/csv", + "html": "text/html", + "htm": "text/html", + "n3": "text/n3", + "txt": "text/plain", + "text": "text/plain", + "conf": "text/plain", + "def": "text/plain", + "list": "text/plain", + "log": "text/plain", + "in": "text/plain", + "dsc": "text/prs.lines.tag", + "rtx": "text/richtext", + "sgml": "text/sgml", + "sgm": "text/sgml", + "tsv": "text/tab-separated-values", + "t": "text/troff", + "tr": "text/troff", + "roff": "text/troff", + "man": "text/troff", + "me": "text/troff", + "ms": "text/troff", + "ttl": "text/turtle", + "uri": "text/uri-list", + "uris": "text/uri-list", + "urls": "text/uri-list", + "vcard": "text/vcard", + "curl": "text/vnd.curl", + "dcurl": "text/vnd.curl.dcurl", + "scurl": "text/vnd.curl.scurl", + "mcurl": "text/vnd.curl.mcurl", + "fly": "text/vnd.fly", + "flx": "text/vnd.fmi.flexstor", + "gv": "text/vnd.graphviz", + "3dml": "text/vnd.in3d.3dml", + "spot": "text/vnd.in3d.spot", + "jad": "text/vnd.sun.j2me.app-descriptor", + "wml": "text/vnd.wap.wml", + "wmls": "text/vnd.wap.wmlscript", + "s": "text/x-asm", + "asm": "text/x-asm", + "c": "text/x-c", + "cc": "text/x-c", + "cxx": "text/x-c", + "cpp": "text/x-c", + "h": "text/x-c", + "hh": "text/x-c", + "dic": "text/x-c", + "f": "text/x-fortran", + "for": "text/x-fortran", + "f77": "text/x-fortran", + "f90": "text/x-fortran", + "p": "text/x-pascal", + "pas": "text/x-pascal", + "java": "text/x-java-source", + "etx": "text/x-setext", + "uu": "text/x-uuencode", + "vcs": "text/x-vcalendar", + "vcf": "text/x-vcard", + "3gp": "video/3gpp", + "3g2": "video/3gpp2", + "h261": "video/h261", + "h263": "video/h263", + "h264": "video/h264", + "jpgv": "video/jpeg", + "jpm": "video/jpm", + "jpgm": "video/jpm", + "mj2": "video/mj2", + "mjp2": "video/mj2", + "mp4": "video/mp4", + "mp4v": "video/mp4", + "mpg4": "video/mp4", + "mpeg": "video/mpeg", + "mpg": "video/mpeg", + "mpe": "video/mpeg", + "m1v": "video/mpeg", + "m2v": "video/mpeg", + "ogv": "video/ogg", + "qt": "video/quicktime", + "mov": "video/quicktime", + "uvh": "video/vnd.dece.hd", + "uvvh": "video/vnd.dece.hd", + "uvm": "video/vnd.dece.mobile", + "uvvm": "video/vnd.dece.mobile", + "uvp": "video/vnd.dece.pd", + "uvvp": "video/vnd.dece.pd", + "uvs": "video/vnd.dece.sd", + "uvvs": "video/vnd.dece.sd", + "uvv": "video/vnd.dece.video", + "uvvv": "video/vnd.dece.video", + "dvb": "video/vnd.dvb.file", + "fvt": "video/vnd.fvt", + "mxu": "video/vnd.mpegurl", + "m4u": "video/vnd.mpegurl", + "pyv": "video/vnd.ms-playready.media.pyv", + "uvu": "video/vnd.uvvu.mp4", + "uvvu": "video/vnd.uvvu.mp4", + "viv": "video/vnd.vivo", + "webm": "video/webm", + "f4v": "video/x-f4v", + "fli": "video/x-fli", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "asf": "video/x-ms-asf", + "asx": "video/x-ms-asf", + "wm": "video/x-ms-wm", + "wmv": "video/x-ms-wmv", + "wmx": "video/x-ms-wmx", + "wvx": "video/x-ms-wvx", + "avi": "video/x-msvideo", + "movie": "video/x-sgi-movie", + "ice": "x-conference/x-cooltalk" +}; \ No newline at end of file diff --git a/websvr/test/lib/mime.js b/websvr/test/lib/mime.js new file mode 100644 index 0000000..28dcbfe --- /dev/null +++ b/websvr/test/lib/mime.js @@ -0,0 +1,93 @@ +var path = require('path'), + fs = require('fs'); + +var mime = module.exports = { + // Map of extension to mime type + types: Object.create(null), + + // Map of mime type to extension + extensions :Object.create(null), + + /** + * Define mimetype -> extension mappings. Each key is a mime-type that maps + * to an array of extensions associated with the type. The first extension is + * used as the default extension for the type. + * + * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); + * + * @param map (Object) type definitions + */ + define: function(map) { + for (var type in map) { + var exts = map[type]; + + for (var i = 0; i < exts.length; i++) { + mime.types[exts[i]] = type; + } + + // Default extension is the first one we encounter + if (!mime.extensions[type]) { + mime.extensions[type] = exts[0]; + } + } + }, + + /** + * Load an Apache2-style ".types" file + * + * This may be called multiple times (it's expected). Where files declare + * overlapping types/extensions, the last file wins. + * + * @param file (String) path of file to load. + */ + load: function(file) { + // Read file and split into lines + var map = {}, + content = fs.readFileSync(file, 'ascii'), + lines = content.split(/[\r\n]+/); + + lines.forEach(function(line, lineno) { + // Clean up whitespace/comments, and split into fields + var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); + map[fields.shift()] = fields; + }); + + mime.define(map); + }, + + /** + * Lookup a mime type based on extension + */ + lookup: function(path, fallback) { + var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); + + return mime.types[ext] || fallback || mime.default_type + }, + + /** + * Return file extension associated with a mime type + */ + extension: function(mimeType) { + return mime.extensions[mimeType]; + }, + + /** + * Lookup a charset based on mime type. + */ + charsets: { + lookup: function (mimeType, fallback) { + // Assume text types are utf8. Modify mime logic as needed. + return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; + } + } +}; + +// Load our local copy of +// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types +mime.load(path.join(__dirname, 'types/mime.types')); + +// Overlay enhancements submitted by the node.js community +mime.load(path.join(__dirname, 'types/node.types')); + +// Set the default type +mime.default_type = mime.types.bin; diff --git a/websvr/test/lib/types.js b/websvr/test/lib/types.js new file mode 100644 index 0000000..ddb92ee --- /dev/null +++ b/websvr/test/lib/types.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/* +Pickup mime type from mime.types files (node-mime); +*/ +var fs = require("fs"), + types = "./types/mime.types"; + +//remove the space/tab/comments +var readLines = function(trunk) { + var lines = []; + + //format the EOL, keep the same with (win & linux) + var body = trunk.toString().replace('\r', ''); + + //remove comments: /* comthing */ + //body = body.replace(/\/\*.*\*\//g, ''); + body.split('\n').forEach(function(line) { + lines.push(line.trim()); + }); + + return lines; +}; + +var pickup = function(lines) { + + var result = {}; + + lines.forEach(function(line, idx) { + // + if (line[0] != '#') { + + //pickup values in attribute + var pair = line.split(/[ \t]+/), + l = pair.length; + + if (l > 1 && pair[0]) { + for (var i = 1; i < l; i++) { + pair[i] && ( result[pair[i]] = pair[0] ); + } + }; + } + }); + + fs.writeFileSync("MIMETYPES.js", "var MIMETYPES = module.exports = " + JSON.stringify(result, null, ' ') + ';'); +}; + +fs.readFile(types, function(err, trunk) { + if (err) throw err; + + var lines = readLines(trunk); + pickup(lines); +}); \ No newline at end of file diff --git a/websvr/test/lib/types/mime.types b/websvr/test/lib/types/mime.types new file mode 100644 index 0000000..cf9dbe8 --- /dev/null +++ b/websvr/test/lib/types/mime.types @@ -0,0 +1,1510 @@ +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpp-ims+xml +# application/activemessage +application/andrew-inset ez +# application/applefile +application/applixware aw +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomicmail +application/atomsvc+xml atomsvc +# application/auth-policy+xml +# application/batch-smtp +# application/beep+xml +# application/calendar+xml +# application/cals-1840 +# application/ccmp+xml +application/ccxml+xml ccxml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cnrp+xml +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csta+xml +# application/cstadata+xml +application/cu-seeme cu +# application/cybercash +application/davmount+xml davmount +# application/dca-rft +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dns +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +application/emma+xml emma +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fits +application/font-tdpfr pfr +# application/framework-attributes+xml +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-request+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +application/json json +# application/kpml-request+xml +# application/kpml-response+xml +application/lost+xml lostxml +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +# application/mathml-content+xml +# application/mathml-presentation+xml +application/mathml+xml mathml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media_control+xml +application/mediaservercontrol+xml mscml +application/metalink4+xml meta4 +application/mets+xml mets +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-request +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nss +# application/ocsp-request +# application/ocsp-response +application/octet-stream bin dms lha lrf lzh so iso dmg dist distz pkg bpk dump elc deploy +application/oda oda +application/oebps-package+xml opf +application/ogg ogx +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/tve-trigger +# application/ulpfec +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp.bsf+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +application/vnd.chemdraw+xml cdxml +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.collection+json +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cybank +application/vnd.data-vision.rdz rdz +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.dir-bi.plate-dl-nosuffix +application/vnd.dna dna +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-request+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.fints +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +# application/vnd.fujixerox.hbpl +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +application/vnd.hzn-3d-crossword x3d +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.packageitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-request+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-request +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.quobject-quoxdocument +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +# application/vnd.otps.ct-kip+xml +application/vnd.palm pdb pqa oprc +# application/vnd.paos.xml +application/vnd.pawaafile paw +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.route66.link66+xml link66 +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +# application/vnd.sun.wadl+xml +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.wsc +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cdlink vcd +application/x-chat chat +application/x-chess-pgn pgn +# application/x-compress +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-ttf ttf ttc +application/x-font-type1 pfa pfb pfm afm +application/x-font-woff woff +# application/x-font-vfont +application/x-futuresplash spl +application/x-gnumeric gnumeric +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-stuffit sit +application/x-stuffitx sitx +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-ustar ustar +application/x-wais-source src +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xpinstall xpi +# application/x400-bp +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info-diff+xml +# application/xcon-conference-info+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xmpp+xml +application/xop+xml xop +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/example +# audio/fwdred +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +audio/ogg oga ogg spx +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu-wb +# audio/pcmu +# audio/prs.sid +# audio/qcelp +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtx +# audio/smv +# audio/smv0 +# audio/smv-qcp +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file dvb +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +audio/x-wav wav +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.dvb.subtitle sub +image/vnd.djvu djvu djv +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +image/vnd.ms-modi mdi +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +image/webp webp +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# model/example +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +model/vnd.vtu vtu +model/vrml wrl vrml +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# text/1d-interleaved-parityfec +text/calendar ics ifb +text/css css +text/csv csv +# text/directory +# text/dns +# text/ecmascript +# text/enriched +# text/example +# text/fwdred +text/html html htm +# text/javascript +text/n3 n3 +# text/parityfec +text/plain txt text conf def list log in +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/vnd.radisys.msml-basic-layout +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.scurl scurl +text/vnd.curl.mcurl mcurl +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-pascal p pas +text/x-java-source java +text/x-setext etx +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raw +# video/rtp-enc-aescm128 +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-ms-asf asf asx +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +x-conference/x-cooltalk ice diff --git a/websvr/test/lib/types/node.types b/websvr/test/lib/types/node.types new file mode 100644 index 0000000..69a6641 --- /dev/null +++ b/websvr/test/lib/types/node.types @@ -0,0 +1,54 @@ +# What: Google Chrome Extension +# Why: To allow apps to (work) be served with the right content type header. +# http://codereview.chromium.org/2830017 +# Added by: niftylettuce +application/x-chrome-extension crx + +# What: OTF Message Silencer +# Why: To silence the "Resource interpreted as font but transferred with MIME +# type font/otf" message that occurs in Google Chrome +# Added by: niftylettuce +font/opentype otf + +# What: HTC support +# Why: To properly render .htc files such as CSS3PIE +# Added by: niftylettuce +text/x-component htc + +# What: HTML5 application cache manifest +# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps +# per https://developer.mozilla.org/en/offline_resources_in_firefox +# Added by: louisremi +text/cache-manifest appcache manifest + +# What: node binary buffer format +# Why: semi-standard extension w/in the node community +# Added by: tootallnate +application/octet-stream buffer + +# What: The "protected" MP-4 formats used by iTunes. +# Why: Required for streaming music to browsers (?) +# Added by: broofa +application/mp4 m4p +audio/mp4 m4a + +# What: Music playlist format (http://en.wikipedia.org/wiki/M3U) +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +application/x-mpegURL m3u8 + +# What: Video format, Part of RFC1890 +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +video/MP2T ts + +# What: The FLAC lossless codec format +# Why: Streaming and serving FLAC audio +# Added by: jacobrask +audio/flac flac + +# What: EventSource mime type +# Why: mime type of Server-Sent Events stream +# http://www.w3.org/TR/eventsource/#text-event-stream +# Added by: francois2metz +text/event-stream event-stream diff --git a/websvr/test/testFormat.js b/websvr/test/testFormat.js new file mode 100644 index 0000000..d27526a --- /dev/null +++ b/websvr/test/testFormat.js @@ -0,0 +1,19 @@ +var test = require("./lib/test"); + +var qs = require("querystring"); + +var testQuery = "a=link&b=1&c=abc&f=efg" +var testString = '{a: "link", b: 1, c: "abc", f: "efg"}'; +var testObject = {a: "link", b: 1, c: "abc", f: "efg"}; + +test("Test parse/stringify querystring", function() { + var string = qs.stringify(testObject); + var obj = qs.parse(string); + //console.log(string, obj); +}); + +test("Test parse/stringify json", function() { + var string = JSON.stringify(testString); + var obj = JSON.parse(string); + //console.log(string, obj); +}); \ No newline at end of file diff --git a/websvr/test/testMIME.js b/websvr/test/testMIME.js new file mode 100644 index 0000000..9e0ed11 --- /dev/null +++ b/websvr/test/testMIME.js @@ -0,0 +1,44 @@ +#!/usr/bin/env node +var test = require("./lib/test"), + mime = require("./lib/mime"); + MIMETYPES = require('./lib/MIMETYPES.js'); + +var urls = [ + "http://www.github.com/appmobi/jq.html", + "stringify/appmobi.lok.mp3", + "good/soha.shtml", + "abcdefg/appmobi.look/abcdefgg.good" + ]; + +/* + It'll be sure that: + 1) The url must indicate to a file. + 2) There will be no query string. + 3) Length of extension will be less than 10. +*/ +var lookup = function(url) { + var idx = url.lastIndexOf('.') + 1, + type; + + (idx > 0 ) && (type = MIMETYPES[url.substr(idx, 10)]); + + return type || "application/octet-stream"; +}; + +test("Test mime.lookup == mime.custom", function() { + urls.forEach(function(url) { + console.log(mime.lookup(url) == lookup(url), lookup(url), mime.lookup(url), url); + }); +}, 1); + +test("Test mime.lookup", function() { + urls.forEach(function(url) { + mime.lookup(url); + }); +}); + +test("Test mime.custom", function() { + urls.forEach(function(url) { + lookup(url); + }); +}); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 502cb09..8b6909d 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -61,7 +61,7 @@ var Settings = { //enable client-side cache(304)? cache: true, //enable debug information output - debug: false, + debug: true, //receive buffer, default size 32k, i.e.: receive post data from ajax request bufferSize: 32768, @@ -100,15 +100,13 @@ Logger: log sth */ var Logger = (function() { - var lineSeparator = "\r\n", + var lineSeparator = "\r\n", indentSeparator = "\t", depth = 9; - var log = function(logObj, dep) { - - var depth = dep || depth; - - var output = new Date() + lineSeparator; + var write = function(logObj, dep) { + var depth = dep || depth, + output = new Date() + lineSeparator; function print(pre, obj) { if (!obj) return; @@ -124,11 +122,37 @@ var Logger = (function() { print(indentSeparator, logObj); fs.appendFile(Settings.logger, output, function(err) { - console.log(err); + log(err); }); }; - return { log: log }; + /* + Currnetly it's equal to console.log + */ + var log = function() { + console.log.apply(console, arguments); + }; + + /* + Add data before log information + */ + var debug = function() { + //diable console.log information + if (!Settings.debug) { + return; + } + + var d = new Date().toString(); + + Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + console.log.apply(console, arguments); + }; + + return { + log: log + , write: write + , debug: debug + }; })(); /*Mapper.js*/ @@ -250,7 +274,7 @@ var SessionManager = (function() { //remove from list delete list[sid]; - console.log("session removed", sid); + Logger.log("session removed", sid); }; /* @@ -280,7 +304,7 @@ var SessionManager = (function() { */ var clean = function() { fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return console.log(err); + if (err) return Logger.log(err); //converted to minutes var expire = (+new Date() - gcTime) / 60000 | 0; @@ -293,7 +317,7 @@ var SessionManager = (function() { //remove the expired session stamp < expire ? remove(file) - : console.log("session skipped", file); + : Logger.log("session skipped", file); } } }); @@ -408,7 +432,7 @@ SessionParser.prototype = { //Write or modify json file fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { if (err) { - console.log(err); + Logger.debug(err); return; } @@ -448,7 +472,7 @@ SessionParser.prototype = { } else if (SessionManager.isValid(self.sid)) { fs.readFile(sessionPath, function(err, data) { if (err) { - console.log(err); + Logger.debug(err); return; }; data = data || "{}"; @@ -513,7 +537,7 @@ var Parser = function(req, res, mapper) { form.parse(req, function(err, fields, files) { if (err) { - console.log(err); + Logger.debug(err); return; }; @@ -605,7 +629,7 @@ FilterChain.prototype = { }, options); */ if (mapper.match(req)) { - console.log("filter matched", self.idx, mapper.expression, req.url); + Logger.debug("filter matched", self.idx, mapper.expression, req.url); //filter matched, parse the request and then execute it Parser(req, res, mapper); @@ -661,10 +685,10 @@ var Handler; var mapper = handlers[i]; if (mapper.match(req)) { - console.log("handler matched", i, mapper.expression, req.url); + Logger.debug("handler matched", i, mapper.expression, req.url); var handler = mapper.handler, - type = handler.constructor.name; + type = handler.constructor.name; switch(type) { //function: treated it as custom function handler @@ -754,7 +778,7 @@ var ListDir = (function() { fs.readdir(dir, function(err, files) { if (err) { listEnd(); - console.log(err); + Logger.debug(err); return; } @@ -770,7 +794,7 @@ var ListDir = (function() { cur++; if (err) { - console.log(err); + Logger.debug(err); }else{ res.write( date(stat.mtime) @@ -806,7 +830,7 @@ var Template = (function() { var fullpath = path.join(Settings.root, filename); fs.readFile(fullpath, function (err, html) { - err && console.log(err); + err && Logger.debug(err); err ? cb("") : cb(html); }); }; @@ -817,7 +841,7 @@ var Template = (function() { tmplFn = engine.compile(chrunk, params); outFn(tmplFn(params)); } catch(err) { - console.log(err); + Logger.debug(err); outFn(err); } }; @@ -967,7 +991,7 @@ var WebSvr = module.exports = (function() { endFn.apply(res, arguments); //Rewirte write/writeHead on response object res.write = res.writeHead = res.setHeader = function() { - console.log("response is already end, response.write ignored!") + Logger.debug("response is already end, response.write ignored!") }; }; @@ -1002,7 +1026,7 @@ var WebSvr = module.exports = (function() { var writeFile = function(res, fullPath) { fs.readFile(fullPath, function(err, data) { if (err) { - console.log(err); + Logger.log(err); return; } res.setHeader("Content-Type", mime.lookup(fullPath)); @@ -1021,9 +1045,6 @@ var WebSvr = module.exports = (function() { self.post = Handler.post; self.session = Handler.session; - //Logger - self.log = Logger.log; - //Get a full path of a request self.getFullPath = function(filePath) { return path.join(root, filePath); @@ -1066,7 +1087,7 @@ var WebSvr = module.exports = (function() { var httpSvr = http.createServer(requestHandler); httpSvr.listen(port); - console.log("Http server running at" + Logger.log("Http server running at" ,"Root:", root ,"Port:", port ); @@ -1082,7 +1103,7 @@ var WebSvr = module.exports = (function() { var httpsSvr = https.createServer(httpsOpts, requestHandler); httpsSvr.listen(httpsPort); - console.log("Https server running at" + Logger.log("Https server running at" ,"Root:", root ,"Port:", httpsPort ); @@ -1090,11 +1111,6 @@ var WebSvr = module.exports = (function() { self.httpsSvr = httpsSvr; } - //diable console.log information - if (!options.debug) { - console.log = function(){}; - } - /* init modules */ @@ -1104,7 +1120,7 @@ var WebSvr = module.exports = (function() { //Public: close http server; self.close = function() { - self.httpSvr && self.httpSvr.close(); + self.httpSvr && self.httpSvr.close(); self.httpsSvr && self.httpsSvr.close(); }; From 37dc449e120c7936a4b001038a04d47a09598c7a Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 24 Jan 2013 10:01:46 +0800 Subject: [PATCH 061/195] Logger Update Logger information in Session --- websvr/Server.js | 2 +- websvr/SessionManager.js | 6 +++--- websvr/websvr.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/websvr/Server.js b/websvr/Server.js index 87c7bad..d1a8df9 100644 --- a/websvr/Server.js +++ b/websvr/Server.js @@ -143,7 +143,7 @@ var WebSvr = module.exports = (function() { var writeFile = function(res, fullPath) { fs.readFile(fullPath, function(err, data) { if (err) { - Logger.log(err); + Logger.debug(err); return; } res.setHeader("Content-Type", mime.lookup(fullPath)); diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js index 064392c..269f695 100644 --- a/websvr/SessionManager.js +++ b/websvr/SessionManager.js @@ -45,7 +45,7 @@ var SessionManager = (function() { //remove from list delete list[sid]; - Logger.log("session removed", sid); + Logger.debug("session removed", sid); }; /* @@ -75,7 +75,7 @@ var SessionManager = (function() { */ var clean = function() { fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.log(err); + if (err) return Logger.debug(err); //converted to minutes var expire = (+new Date() - gcTime) / 60000 | 0; @@ -88,7 +88,7 @@ var SessionManager = (function() { //remove the expired session stamp < expire ? remove(file) - : Logger.log("session skipped", file); + : Logger.debug("session skipped", file); } } }); diff --git a/websvr/websvr.js b/websvr/websvr.js index 8b6909d..07991ff 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -274,7 +274,7 @@ var SessionManager = (function() { //remove from list delete list[sid]; - Logger.log("session removed", sid); + Logger.debug("session removed", sid); }; /* @@ -304,7 +304,7 @@ var SessionManager = (function() { */ var clean = function() { fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.log(err); + if (err) return Logger.debug(err); //converted to minutes var expire = (+new Date() - gcTime) / 60000 | 0; @@ -317,7 +317,7 @@ var SessionManager = (function() { //remove the expired session stamp < expire ? remove(file) - : Logger.log("session skipped", file); + : Logger.debug("session skipped", file); } } }); @@ -1026,7 +1026,7 @@ var WebSvr = module.exports = (function() { var writeFile = function(res, fullPath) { fs.readFile(fullPath, function(err, data) { if (err) { - Logger.log(err); + Logger.debug(err); return; } res.setHeader("Content-Type", mime.lookup(fullPath)); From 75125dbb30cda05c5c19115ddd92524e5d2db5c9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 24 Jan 2013 10:29:28 +0800 Subject: [PATCH 062/195] Update Update logging date format. --- websvr/Logger.js | 8 +++++--- websvr/websvr.js | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/websvr/Logger.js b/websvr/Logger.js index 2077de8..ab6aa77 100644 --- a/websvr/Logger.js +++ b/websvr/Logger.js @@ -7,6 +7,8 @@ var Logger = (function() { indentSeparator = "\t", depth = 9; + var Settings = Settings || { debug: true }; + var write = function(logObj, dep) { var depth = dep || depth, output = new Date() + lineSeparator; @@ -45,13 +47,13 @@ var Logger = (function() { return; } - var d = new Date().toString(); + var d = new Date(); - Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + Array.prototype.splice.call(arguments, 0, 0, d.toISOString()); console.log.apply(console, arguments); }; - return { + return { log: log , write: write , debug: debug diff --git a/websvr/websvr.js b/websvr/websvr.js index 07991ff..05d0f7a 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -104,6 +104,8 @@ var Logger = (function() { indentSeparator = "\t", depth = 9; + var Settings = Settings || { debug: true }; + var write = function(logObj, dep) { var depth = dep || depth, output = new Date() + lineSeparator; @@ -142,13 +144,13 @@ var Logger = (function() { return; } - var d = new Date().toString(); + var d = new Date(); - Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + Array.prototype.splice.call(arguments, 0, 0, d.toISOString()); console.log.apply(console, arguments); }; - return { + return { log: log , write: write , debug: debug From 06397911469441e1e5f9265ce88847448f946f53 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 16 Apr 2013 23:17:32 +0800 Subject: [PATCH 063/195] Update Add multi-instance support. Using system temp folder to store session & upload file. --- website/start.cmd | 20 +- website/start.sh | 22 +- websvr/BodyParser.js | 20 - websvr/Filter.js | 81 - websvr/Global.js | 27 - websvr/Handler.js | 81 - websvr/ListDir.js | 94 - websvr/Logger.js | 62 - websvr/MakeFile.list | 22 - websvr/Mapper.js | 49 - websvr/Parser.js | 66 - websvr/Server.js | 248 --- websvr/SessionManager.js | 130 -- websvr/SessionParser.js | 131 -- websvr/Settings.js | 51 - websvr/Template.js | 62 - websvr/Utility.js | 15 - websvr/build.cmd | 5 - websvr/build.sh | 6 - websvr/lib/file.js | 145 +- websvr/lib/incoming_form.js | 921 +++++---- websvr/lib/json_parser.js | 35 + websvr/lib/mime.js | 207 +- websvr/lib/multipart_parser.js | 636 +++--- websvr/lib/octet_parser.js | 20 + websvr/lib/querystring_parser.js | 52 +- websvr/lib/types/mime.types | 3098 +++++++++++++++--------------- websvr/lib/types/node.types | 125 +- websvr/lib/util.js | 6 - websvr/tool/Combine.js | 226 --- websvr/websvr.js | 2275 +++++++++++----------- 31 files changed, 3949 insertions(+), 4989 deletions(-) delete mode 100644 websvr/BodyParser.js delete mode 100644 websvr/Filter.js delete mode 100644 websvr/Global.js delete mode 100644 websvr/Handler.js delete mode 100644 websvr/ListDir.js delete mode 100644 websvr/Logger.js delete mode 100644 websvr/MakeFile.list delete mode 100644 websvr/Mapper.js delete mode 100644 websvr/Parser.js delete mode 100644 websvr/Server.js delete mode 100644 websvr/SessionManager.js delete mode 100644 websvr/SessionParser.js delete mode 100644 websvr/Settings.js delete mode 100644 websvr/Template.js delete mode 100644 websvr/Utility.js delete mode 100644 websvr/build.cmd delete mode 100755 websvr/build.sh create mode 100644 websvr/lib/json_parser.js create mode 100644 websvr/lib/octet_parser.js delete mode 100644 websvr/lib/util.js delete mode 100644 websvr/tool/Combine.js diff --git a/website/start.cmd b/website/start.cmd index 89a808b..b3fccc6 100644 --- a/website/start.cmd +++ b/website/start.cmd @@ -1,9 +1,11 @@ -REM start sitetest - -:loop -node svr/sitetest.js - -REM waiting... -pause; - -goto loop; \ No newline at end of file +REM start sitetest + +:loop + node svr/sitetest.js + + REM *************************************** + REM Server stop working, restarting... + REM *************************************** + + ping -n 2 127.1>nul +goto loop \ No newline at end of file diff --git a/website/start.sh b/website/start.sh index 8a1f459..dca9911 100644 --- a/website/start.sh +++ b/website/start.sh @@ -1,9 +1,15 @@ -#!/bin/bash -echo "start sitetest" - -while true; do - node svr/sitetest.js - - echo "exit, press any kep to continue..." - read -N1 +#!/bin/bash +echo start sitetest + +#change current dir, in order to call it from anywhere. +cd $(dirname $0) + +while true; do + node svr/sitetest.js + + echo *************************************** + echo Server stop working, restarting... + echo *************************************** + + sleep 2 done \ No newline at end of file diff --git a/websvr/BodyParser.js b/websvr/BodyParser.js deleted file mode 100644 index cb2ea54..0000000 --- a/websvr/BodyParser.js +++ /dev/null @@ -1,20 +0,0 @@ -/* -Body parser, parse the data in request body via -when parse complete, execute the callback, with response data; -*/ -var BodyParser = function(req, res, callback) { - - var buffer = new Buffer(Settings.bufferSize); - - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); -}; \ No newline at end of file diff --git a/websvr/Filter.js b/websvr/Filter.js deleted file mode 100644 index a0319b2..0000000 --- a/websvr/Filter.js +++ /dev/null @@ -1,81 +0,0 @@ -/* -Http Filter: Execute all the rules that matched, -Filters will be always called before a handler. -*/ -var Filter = { - //filter list - filters: [], - - /* - filter: add a new filter - expression: string/regexp [optional] - handler: function [required] - options: object [optional] - */ - filter: function(expression, handler, options) { - //The first parameter is Function => (handler, options) - if (expression.constructor == Function) { - options = handler; - handler = expression; - expression = null; - } - - var mapper = new Mapper(expression, handler, options); - Filter.filters.push(mapper); - }, - - /* - file receiver: it's a specfic filter, - this filter should be always at the top of the filter list - */ - file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); - //insert at the top of the filter array - Filter.filters.splice(0, 0, mapper); - } -}; - -/* -Filter Chain -*/ -var FilterChain = function(cb, req, res) { - var self = this; - - self.idx = 0; - self.cb = cb; - - self.req = req; - self.res = res; -}; - -FilterChain.prototype = { - next: function() { - var self = this, - req = self.req, - res = self.res; - - var mapper = Filter.filters[self.idx++]; - - //filter is complete, execute callback; - if (!mapper) return self.cb && self.cb(); - - /* - If not Matched go to next filter - If matched need to execute the req.next() in callback handler, - e.g: - webSvr.filter(/expression/, function(req, res) { - //filter actions - req.next(req, res); - }, options); - */ - if (mapper.match(req)) { - Logger.debug("filter matched", self.idx, mapper.expression, req.url); - - //filter matched, parse the request and then execute it - Parser(req, res, mapper); - }else{ - //filter not matched, validate next filter - self.next(); - } - } -}; \ No newline at end of file diff --git a/websvr/Global.js b/websvr/Global.js deleted file mode 100644 index c7efccb..0000000 --- a/websvr/Global.js +++ /dev/null @@ -1,27 +0,0 @@ -/* -* Description: node-websvr -* Author: Kris Zhang -* Licenses: MIT, GPL -* Project url: https://github.com/newghost/node-websvr -* -* Referenced projects: -* Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -* Underscore: Add underscore a utility-belt library for JavaScript - https://github.com/documentcloud/underscore -* MIME: content-type in header - https://github.com/broofa/node-mime -* template: Template Engine - https://github.com/olado/doT -*/ - -//Node libraries -var fs = require("fs"); -var path = require("path"); -var qs = require("querystring"); - -var http = require("http"); -var https = require("https"); - -//Open source libraries -var mime = require("./lib/mime"); \ No newline at end of file diff --git a/websvr/Handler.js b/websvr/Handler.js deleted file mode 100644 index 04d9edd..0000000 --- a/websvr/Handler.js +++ /dev/null @@ -1,81 +0,0 @@ -/* -Http Handler: Execute and returned when when first matched; -At the same time only one Handler will be called; -*/ -var Handler; - -(function() { - - /* - Private: handler list - */ - var handlers = []; - - /* - Static Handler instance - */ - Handler = { - /* - url: add a new handler - expression: string/regexp [required] - handler: [many types] [required] - options: object [optional] - */ - url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - handlers.push(mapper); - }, - - //Post: Parse the post data by default; - post: function(expression, handler, options) { - this.url(expression, handler, _.extend({ parse: true }, options)); - }, - - //Session: Parse the session and post by default; - session: function(expression, handler) { - this.url(expression, handler, { parse: true, session: true }); - }, - - handle: function(req, res) { - //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++) { - - var mapper = handlers[i]; - if (mapper.match(req)) { - - Logger.debug("handler matched", i, mapper.expression, req.url); - - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type) { - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - return true; - } - } - - return false; - - } //end of handle - - }; - -}()); - - - diff --git a/websvr/ListDir.js b/websvr/ListDir.js deleted file mode 100644 index 98518db..0000000 --- a/websvr/ListDir.js +++ /dev/null @@ -1,94 +0,0 @@ -/* -ListDir: List all the files in a directory -*/ -var ListDir = (function() { - - var urlFormat = function(url) { - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date) { - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num) { - return num + " ".substring(0, 12 - String(num).length); - }; - - //Create an anchor - var anchor = function(txt, url) { - url = url ? url : "/"; - return '' + txt + ""; - }; - - var listDir = { - //List all the files in a directory - list: function(req, res, dir) { - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function() { - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function() {
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files) { - if (err) { - listEnd(); - Logger.debug(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++) { - //Persistent the idx before make the sync process - (function(idx) { - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat) { - cur++; - - if (err) { - Logger.debug(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - } - }; - - return listDir; - -}()); \ No newline at end of file diff --git a/websvr/Logger.js b/websvr/Logger.js deleted file mode 100644 index ab6aa77..0000000 --- a/websvr/Logger.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -Logger: log sth -*/ -var Logger = (function() { - - var lineSeparator = "\r\n", - indentSeparator = "\t", - depth = 9; - - var Settings = Settings || { debug: true }; - - var write = function(logObj, dep) { - var depth = dep || depth, - output = new Date() + lineSeparator; - - function print(pre, obj) { - if (!obj) return; - for (var key in obj) { - var val = obj[key]; - output = output + pre + key + " : " + val + lineSeparator; - if (typeof val == "object") { - (pre.length < depth) && print(pre + indentSeparator, val); - } - } - } - - print(indentSeparator, logObj); - - fs.appendFile(Settings.logger, output, function(err) { - log(err); - }); - }; - - /* - Currnetly it's equal to console.log - */ - var log = function() { - console.log.apply(console, arguments); - }; - - /* - Add data before log information - */ - var debug = function() { - //diable console.log information - if (!Settings.debug) { - return; - } - - var d = new Date(); - - Array.prototype.splice.call(arguments, 0, 0, d.toISOString()); - console.log.apply(console, arguments); - }; - - return { - log: log - , write: write - , debug: debug - }; - -})(); \ No newline at end of file diff --git a/websvr/MakeFile.list b/websvr/MakeFile.list deleted file mode 100644 index 0f5c246..0000000 --- a/websvr/MakeFile.list +++ /dev/null @@ -1,22 +0,0 @@ -#Config file -#These modules will be crunched to one. -#TODO: Need to support dictionary in the list; - -#Global object and description -Global.js -Utility.js - -Settings.js -Logger.js - -#WebSvr Modules -Mapper.js -BodyParser.js -SessionManager.js -SessionParser.js -Parser.js -Filter.js -Handler.js -ListDir.js -Template.js -Server.js \ No newline at end of file diff --git a/websvr/Mapper.js b/websvr/Mapper.js deleted file mode 100644 index 4af881c..0000000 --- a/websvr/Mapper.js +++ /dev/null @@ -1,49 +0,0 @@ -/* -Mapper: Used for Filter & Handler, -expression: required parameter -handler: required parameter -options: optional parameters -*/ - -var Mapper = function(expression, handler, options) { - var self = this; - - self.expression = expression; - self.handler = handler; - - //Has other parameters? - self.extend(options); -}; - -Mapper.prototype = { - /* - Does this mapper matched this request? - */ - match: function(req) { - var self = this, - expression = self.expression; - - //No expression? It's a general filter mapper - if (!expression) return true; - - switch (expression.constructor) { - case String: return req.url.indexOf(expression) > -1; - case RegExp: return expression.test(req.url); - } - - return false; - }, - - /* - Add optional parameters on current mapper - i.e: - session: boolean - file: boolean - parse: boolean - */ - extend: function(options) { - for(key in options) { - this[key] = options[key] - } - } -}; \ No newline at end of file diff --git a/websvr/Parser.js b/websvr/Parser.js deleted file mode 100644 index 9b3e17c..0000000 --- a/websvr/Parser.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -Parser: Functions that Filter and Handler will be called -*/ -var Parser = function(req, res, mapper) { - - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - req.session = new SessionParser(req, res); - } - handler(req, res); - }; - - /* - parse data in request, this should be done before parse session, - because session stored in file - */ - var parseBody = function() { - //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { - //Must parser the request first, or the post data will lost; - BodyParser(req, res, function(data) { - req.body = data; - parseSession(); - }); - }else{ - parseSession(); - } - }; - - /* - parse file in request, this should be at the top of the list - */ - var parseFile = function() { - //Need to parse the file in request? - if (mapper.file && typeof req.body == "undefined") { - //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'), - form = new formidable.IncomingForm(); - - form.uploadDir = Settings.uploadDir; - - form.parse(req, function(err, fields, files) { - if (err) { - Logger.debug(err); - return; - }; - - //attach the parameters and files - req.body = fields; - req.files = files; - - //in fact request will not be parsed again, because body is not undefined - parseBody(); - }); - }else{ - parseBody(); - }; - }; - - parseFile(); - -}; diff --git a/websvr/Server.js b/websvr/Server.js deleted file mode 100644 index d1a8df9..0000000 --- a/websvr/Server.js +++ /dev/null @@ -1,248 +0,0 @@ -/* -* Description: Create a Web Server -* Author: Kris Zhang -* Licenses: MIT, GPL -*/ -/* -* Define and Export WebSvr -*/ -var WebSvr = module.exports = (function() { - - var server = function(options) { - - //Parameters - //Count: How many files? - var self = this, - root, - port; - - var fileHandler = function(req, res) { - - var url = req.url, - hasQuery = url.indexOf("?"); - - //fs.stat can't recognize the file name with querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(root, url); - - //Handle path - var handlePath = function(phyPath) { - fs.stat(phyPath, function(err, stat) { - - //Consider as file not found - if (err) return self.write404(res); - - //Is file? Open this file and send to client. - if (stat.isFile()) { - // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 - var cacheTime = new Date(req.headers["if-modified-since"] || 1); - - // The file is modified - if (Settings.cache && stat.mtime <= cacheTime) { - res.writeHead(304); - res.end(); - - // Else send "not modifed" - } else { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, phyPath); - } - } - - //Is Directory? - else if (stat.isDirectory()) { - handleDefault(phyPath); - } - - //Or write the 404 pages - else { - self.write404(res); - } - - }); - }; - - //List all the files and folders. - var handleDir = function(dirPath) { - options.listDir - ? ListDir.list(req, res, dirPath) - : self.write403(res); - }; - - //Handle default page - var handleDefault = function(dirPath) { - var defaultPage = options.defaultPage; - - if (defaultPage) { - var defaultPath = path.join(dirPath, defaultPage); - - fs.exists(defaultPath, function (exists) { - //If page exists hanle it again - if (exists) { - //In order to make it as a dir path for loading static resources - if (url[url.length - 1] != '/') { - return res.redirect(url + '/'); - } - - handlePath(defaultPath); - //If page doesn't exist hanlde the dir again - } else { - handleDir(dirPath); - } - }); - } else { - handleDir(dirPath); - } - }; - - handlePath(fullPath); - }; - - var requestHandler = function(req, res) { - //Response may be shutdown when do the filter, in order not to cause exception, - //Rewrite the write/writeHead functionalities of current response object - var endFn = res.end; - res.end = function() { - //Execute old end - endFn.apply(res, arguments); - //Rewirte write/writeHead on response object - res.write = res.writeHead = res.setHeader = function() { - Logger.debug("response is already end, response.write ignored!") - }; - }; - - res.writeFile = function(filePath, cb) { - self.writeFile(res, filePath, cb); - }; - - //301/302 : move permanently - res.redirect = function(url, status) { - res.writeHead(status ? status : 302, { "Location": url }); - res.end(); - }; - - //render template objects - res.render = Template.render; - - //initial httprequest - var filterChain = new FilterChain(function(){ - - //if handler not match, send the request - !Handler.handle(req, res) && fileHandler(req, res); - - }, req, res); - - //Hook FilterChain object on the request - req.filter = filterChain; - - //Handle the first filter - req.filter.next(); - }; - - var writeFile = function(res, fullPath) { - fs.readFile(fullPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - } - res.setHeader("Content-Type", mime.lookup(fullPath)); - res.writeHead(200); - res.end(data, "binary"); - }); - }; - - //Explose API - //Filter - self.filter = Filter.filter; - self.file = Filter.file; - - //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; - - //Get a full path of a request - self.getFullPath = function(filePath) { - return path.join(root, filePath); - }; - - //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb) { - filePath = path.join(root, filePath); - fs.exists(filePath, function(exist) { - if (exist) { - writeFile(res, filePath); - cb && cb(exist); - }else{ - //If callback function doesn't exist, write 404 page; - cb ? cb(exist) : self.write404(res); - } - }); - }; - - self.write403 = function(res) { - res.writeHead(403, {"Content-Type": "text/html"}); - res.end("Access forbidden!"); - }; - - self.write404 = function(res) { - res.writeHead(404, {"Content-Type": "text/html"}); - res.end("File not found!"); - }; - - //Public: start http server - self.start = function() { - //Update the default value of Settings - options = _.extend(Settings, options); - - root = options.root; - port = parseInt(options.port); - - //Create http server - if (options.http) { - var httpSvr = http.createServer(requestHandler); - httpSvr.listen(port); - - Logger.log("Http server running at" - ,"Root:", root - ,"Port:", port - ); - - self.httpSvr = httpSvr; - } - - //Create https server - if (options.https) { - var httpsOpts = options.httpsOpts, - httpsPort = options.httpsPort; - - var httpsSvr = https.createServer(httpsOpts, requestHandler); - httpsSvr.listen(httpsPort); - - Logger.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); - - self.httpsSvr = httpsSvr; - } - - /* - init modules - */ - //Start session garbage collection - SessionManager.start(); - }; - - //Public: close http server; - self.close = function() { - self.httpSvr && self.httpSvr.close(); - self.httpsSvr && self.httpsSvr.close(); - }; - - }; - - return server; - -})(); \ No newline at end of file diff --git a/websvr/SessionManager.js b/websvr/SessionManager.js deleted file mode 100644 index 269f695..0000000 --- a/websvr/SessionManager.js +++ /dev/null @@ -1,130 +0,0 @@ -/* -SessionManager: -- Clear expired session files -- Valid session -*/ -var SessionManager = (function() { - - //duration time - var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; - - //timer - var timer; - - //session array object, stored with {sid: [update time]}; - var list = {}; - - var getPath = function(sid) { - return path.join(Settings.sessionDir, sid); - }; - - //create a new session id - var create = function() { - //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions - var uuid - = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars - + '-' - + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars - + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars - - //fix the length to 25 - uuid += '00000000000000000000'.substr(0, 25 - uuid.length); - - return uuid; - }; - - //force update session in list - var update = function(sid, datetime) { - list[sid] = datetime || new Date(); - }; - - //remove a sesson from list - var remove = function(sid) { - //delete the file - fs.unlink(getPath(sid)); - //remove from list - delete list[sid]; - - Logger.debug("session removed", sid); - }; - - /* - Does session expired? - If the session is not in the list, add to the list. - i.e. When WebSvr restarted, session will not expired. - */ - var isValid = function(sid) { - var now = new Date(); - - !list[sid] && (list[sid] = now); - - return now - list[sid] <= Settings.sessionTimeout - }; - - /* - Session clean handler - */ - var cleanHandler = function() { - for (var sid in list) { - !isValid(sid) && remove(sid); - } - }; - - /* - Clean the session in temp folder - */ - var clean = function() { - fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.debug(err); - - //converted to minutes - var expire = (+new Date() - gcTime) / 60000 | 0; - - files.forEach(function(file) { - if (file.length == 25) { - var stamp = parseInt(file.substr(0, file.indexOf('-'))); - - if (stamp) { - //remove the expired session - stamp < expire - ? remove(file) - : Logger.debug("session skipped", file); - } - } - }); - }); - }; - - //refresh session in list, valid first, if not expired, update the time - var refresh = function(sid, datetime) { - isValid(sid) && update(sid, datetime); - }; - - var stop = function() { - clearInterval(timer); - timer = null; - }; - - //stop before new session start - var start = function() { - //stop cleanHandler if available - stop(); - //clean the old sessions - clean(); - timer = setInterval(cleanHandler, gcTime); - }; - - return { - list: list, - create: create, - update: update, - remove: remove, - refresh: refresh, - isValid: isValid, - getPath: getPath, - clean: clean, - start: start, - stop: stop - } - -})(); \ No newline at end of file diff --git a/websvr/SessionParser.js b/websvr/SessionParser.js deleted file mode 100644 index 2236cff..0000000 --- a/websvr/SessionParser.js +++ /dev/null @@ -1,131 +0,0 @@ -/* -Parse request with session support -*/ -var SessionParser = function(req, res) { - var self = this; - - //session id - self.sid = null; - //session stored object - self.obj = null; - //is this new session? - self.new = false; - - //init session object - self.init(req, res); -}; - -SessionParser.prototype = { - init: function(req, res) { - var self = this, - sidKey = "_wsid", - sidVal; - - //Get or Create sid, sid exist in the cookie, read it - var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); - - //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 25) { - sidVal = SessionManager.create(); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - self.new = true; - }; - self.sid = sidVal; - - SessionManager.refresh(self.sid); - } - - //Create new session object - , newObj: function(key, cb) { - //Key is offered, return null of this key, else return empty session object - var self = this, - val = key ? null : {}; - - self.obj = {}; - cb && cb(val); - return val; - } - - //Get value from session object - , getVal: function(key, cb) { - var self = this; - - //key is null, return all the session object - var val = key ? self.obj[key] : self.obj; - cb && cb(val); - - return val; - } - - //Set an key/value pair in session object - , set: function(key, val, cb) { - var self = this; - - //Get session object first - self.get(function() { - - //Add or update key/value in session object - self.obj[key] = val; - - //Write or modify json file - fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { - if (err) { - Logger.debug(err); - return; - } - - cb && cb(self.obj); - - //force update - SessionManager.update(self.sid); - }); - }); - } - - //Get value from session file - , get: function(key, cb) { - var self = this; - - //The first parameter is callback function - if (key.constructor == Function) { - cb = key; - key = null; - } - - //The session object is already loaded - if (self.obj) return self.getVal(key, cb); - - //It's a new session file, need not to load it from file - if (self.new) return self.newObj(key, cb); - - var sessionPath = SessionManager.getPath(self.sid); - - //File operates, will cause delay - fs.exists(sessionPath, function(exists) { - //err: file doesn't exist - if (!exists) { - return self.newObj(key, cb); - - //session not expired - } else if (SessionManager.isValid(self.sid)) { - fs.readFile(sessionPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - return self.getVal(key, cb); - }); - - //session expired, treat it as new session - } else { - return self.newObj(key, cb); - } - }); - } - -}; \ No newline at end of file diff --git a/websvr/Settings.js b/websvr/Settings.js deleted file mode 100644 index d60b659..0000000 --- a/websvr/Settings.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Configurations -*/ -var Settings = { - //root folder of web - root: "../", - - //list files in directory - listDir: false, - - //http - http: true, - //default port of http - port: 8054, - - //enable client-side cache(304)? - cache: true, - //enable debug information output - debug: true, - //receive buffer, default size 32k, i.e.: receive post data from ajax request - bufferSize: 32768, - - //default pages, only one is supported - defaultPage: "index.html", - - //https - https: false, - //default port of https - httpsPort: 8443, - httpsOpts: { key:"", cert:"" }, - - //logger file path - logger: "./tmp/log.txt", - - //session file stored here - sessionDir: "./tmp/session", - /* - Session timeout, in milliseconds. - When session is expired, session file will not deleted. - */ - sessionTimeout: 1440000, - /* - Session garbage collection time, in milliseconds. - When session expired time is more than (sessionAge + sessionGCT), - then session file will be deleted. - */ - sessionGarbage: 3460000, - - //tempary upload file stored here - uploadDir: "./tmp/upload" -}; \ No newline at end of file diff --git a/websvr/Template.js b/websvr/Template.js deleted file mode 100644 index 5d89cd9..0000000 --- a/websvr/Template.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -* Templates -*/ -var Template = (function() { - - var engine = require("./lib/doT"); - - //get a file - var getFile = function(filename, cb){ - var fullpath = path.join(Settings.root, filename); - - fs.readFile(fullpath, function (err, html) { - err && Logger.debug(err); - err ? cb("") : cb(html); - }); - }; - - //render a file - var render = function(chrunk, params, outFn){ - try { - tmplFn = engine.compile(chrunk, params); - outFn(tmplFn(params)); - } catch(err) { - Logger.debug(err); - outFn(err); - } - }; - - return { - //render templates - render: function(chrunk, params) { - var res = this, - end = res.end; - - var url = chrunk.url, - con = chrunk.constructor; - - //It's a http request (it has "url") - if (url) { - getFile(url, function(tmpl) { - render(tmpl, params, end); - }); - - //It's html contents (template codes) - } else if (con == String) { - render(chrunk, params, end); - - //It's Array object (template file path) - } else if (con == Array) { - getFile(chrunk[0], function(tmpl) { - render(tmpl, params, end); - }); - - //Nothing matched end the response - } else { - end(); - } - - } - } - -}()); \ No newline at end of file diff --git a/websvr/Utility.js b/websvr/Utility.js deleted file mode 100644 index 58ae734..0000000 --- a/websvr/Utility.js +++ /dev/null @@ -1,15 +0,0 @@ -/* -* Utility -*/ -var _ = { - //extend object to target - extend: function(tar, obj) { - if (!obj) return; - - for (var key in obj) { - tar[key] = obj[key]; - } - - return tar; - } -}; \ No newline at end of file diff --git a/websvr/build.cmd b/websvr/build.cmd deleted file mode 100644 index ef87895..0000000 --- a/websvr/build.cmd +++ /dev/null @@ -1,5 +0,0 @@ -REM Build websvr JS, and keep watching the changes -node tool/Combine.js -i MakeFile.list -o websvr.js -w - -REM Combine complete, Goodbye. -pause; \ No newline at end of file diff --git a/websvr/build.sh b/websvr/build.sh deleted file mode 100755 index 16cd5e6..0000000 --- a/websvr/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -echo "Build websvr JS, and keep watching the changes" -node tool/Combine.js -i MakeFile.list -o websvr.js -w - -echo "Combine complete, Goodbye." -sleep 10 \ No newline at end of file diff --git a/websvr/lib/file.js b/websvr/lib/file.js index 362019c..e34c10e 100644 --- a/websvr/lib/file.js +++ b/websvr/lib/file.js @@ -1,73 +1,72 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -var util = require('./util'), - WriteStream = require('fs').WriteStream, - EventEmitter = require('events').EventEmitter, - crypto = require('crypto'); - -function File(properties) { - EventEmitter.call(this); - - this.size = 0; - this.path = null; - this.name = null; - this.type = null; - this.hash = null; - this.lastModifiedDate = null; - - this._writeStream = null; - - for (var key in properties) { - this[key] = properties[key]; - } - - if(typeof this.hash === 'string') { - this.hash = crypto.createHash(properties.hash); - } - - this._backwardsCompatibility(); -} -module.exports = File; -util.inherits(File, EventEmitter); - -// @todo Next release: Show error messages when accessing these -File.prototype._backwardsCompatibility = function() { - var self = this; - this.__defineGetter__('length', function() { - return self.size; - }); - this.__defineGetter__('filename', function() { - return self.name; - }); - this.__defineGetter__('mime', function() { - return self.type; - }); -}; - -File.prototype.open = function() { - this._writeStream = new WriteStream(this.path); -}; - -File.prototype.write = function(buffer, cb) { - var self = this; - this._writeStream.write(buffer, function() { - if(self.hash) { - self.hash.update(buffer); - } - self.lastModifiedDate = new Date(); - self.size += buffer.length; - self.emit('progress', self.size); - cb(); - }); -}; - -File.prototype.end = function(cb) { - var self = this; - this._writeStream.end(function() { - if(self.hash) { - self.hash = self.hash.digest('hex'); - } - self.emit('end'); - cb(); - }); -}; +if (global.GENTLY) require = GENTLY.hijack(require); + +var util = require('util'), + WriteStream = require('fs').WriteStream, + EventEmitter = require('events').EventEmitter, + crypto = require('crypto'); + +function File(properties) { + EventEmitter.call(this); + + this.size = 0; + this.path = null; + this.name = null; + this.type = null; + this.hash = null; + this.lastModifiedDate = null; + + this._writeStream = null; + + for (var key in properties) { + this[key] = properties[key]; + } + + if(typeof this.hash === 'string') { + this.hash = crypto.createHash(properties.hash); + } else { + this.hash = null; + } +} +module.exports = File; +util.inherits(File, EventEmitter); + +File.prototype.open = function() { + this._writeStream = new WriteStream(this.path); +}; + +File.prototype.toJSON = function() { + return { + size: this.size, + path: this.path, + name: this.name, + type: this.type, + mtime: this.lastModifiedDate, + length: this.length, + filename: this.filename, + mime: this.mime + }; +}; + +File.prototype.write = function(buffer, cb) { + var self = this; + if (self.hash) { + self.hash.update(buffer); + } + this._writeStream.write(buffer, function() { + self.lastModifiedDate = new Date(); + self.size += buffer.length; + self.emit('progress', self.size); + cb(); + }); +}; + +File.prototype.end = function(cb) { + var self = this; + if (self.hash) { + self.hash = self.hash.digest('hex'); + } + this._writeStream.end(function() { + self.emit('end'); + cb(); + }); +}; diff --git a/websvr/lib/incoming_form.js b/websvr/lib/incoming_form.js index 76eb944..e94d5be 100644 --- a/websvr/lib/incoming_form.js +++ b/websvr/lib/incoming_form.js @@ -1,390 +1,531 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -var fs = require('fs'); -var util = require('./util'), - path = require('path'), - File = require('./file'), - MultipartParser = require('./multipart_parser').MultipartParser, - QuerystringParser = require('./querystring_parser').QuerystringParser, - StringDecoder = require('string_decoder').StringDecoder, - EventEmitter = require('events').EventEmitter, - Stream = require('stream').Stream; - -function IncomingForm(opts) { - if (!(this instanceof IncomingForm)) return new IncomingForm; - EventEmitter.call(this); - - opts=opts||{}; - - this.error = null; - this.ended = false; - - this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; - this.keepExtensions = opts.keepExtensions || false; - this.uploadDir = opts.uploadDir || IncomingForm.UPLOAD_DIR; - this.encoding = opts.encoding || 'utf-8'; - this.headers = null; - this.type = null; - this.hash = false; - - this.bytesReceived = null; - this.bytesExpected = null; - - this._parser = null; - this._flushing = 0; - this._fieldsSize = 0; -}; -util.inherits(IncomingForm, EventEmitter); -exports.IncomingForm = IncomingForm; - -IncomingForm.UPLOAD_DIR = (function() { - var dirs = [ - process.env.TMP, - process.env.TMPDIR, - process.env.TEMP, - '/tmp', - process.cwd() - ]; - for (var i = 0; i < dirs.length; i++) { - var dir = dirs[i]; - var isDirectory = false; - - try { - isDirectory = fs.statSync(dir).isDirectory(); - } catch (e) {} - - if (isDirectory) return dir; - } -})(); - -IncomingForm.prototype.parse = function(req, cb) { - this.pause = function() { - try { - req.pause(); - } catch (err) { - // the stream was destroyed - if (!this.ended) { - // before it was completed, crash & burn - this._error(err); - } - return false; - } - return true; - }; - - this.resume = function() { - try { - req.resume(); - } catch (err) { - // the stream was destroyed - if (!this.ended) { - // before it was completed, crash & burn - this._error(err); - } - return false; - } - - return true; - }; - - this.writeHeaders(req.headers); - - var self = this; - req - .on('error', function(err) { - self._error(err); - }) - .on('aborted', function() { - self.emit('aborted'); - }) - .on('data', function(buffer) { - self.write(buffer); - }) - .on('end', function() { - if (self.error) { - return; - } - - var err = self._parser.end(); - if (err) { - self._error(err); - } - }); - - if (cb) { - var fields = {}, files = {}; - this - .on('field', function(name, value) { - fields[name] = value; - }) - .on('file', function(name, file) { - files[name] = file; - }) - .on('error', function(err) { - cb(err, fields, files); - }) - .on('end', function() { - cb(null, fields, files); - }); - } - - return this; -}; - -IncomingForm.prototype.writeHeaders = function(headers) { - this.headers = headers; - this._parseContentLength(); - this._parseContentType(); -}; - -IncomingForm.prototype.write = function(buffer) { - if (!this._parser) { - this._error(new Error('unintialized parser')); - return; - } - - this.bytesReceived += buffer.length; - this.emit('progress', this.bytesReceived, this.bytesExpected); - - var bytesParsed = this._parser.write(buffer); - if (bytesParsed !== buffer.length) { - this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); - } - - return bytesParsed; -}; - -IncomingForm.prototype.pause = function() { - // this does nothing, unless overwritten in IncomingForm.parse - return false; -}; - -IncomingForm.prototype.resume = function() { - // this does nothing, unless overwritten in IncomingForm.parse - return false; -}; - -IncomingForm.prototype.onPart = function(part) { - // this method can be overwritten by the user - this.handlePart(part); -}; - -IncomingForm.prototype.handlePart = function(part) { - var self = this; - - if (part.filename === undefined) { - var value = '' - , decoder = new StringDecoder(this.encoding); - - part.on('data', function(buffer) { - self._fieldsSize += buffer.length; - if (self._fieldsSize > self.maxFieldsSize) { - self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); - return; - } - value += decoder.write(buffer); - }); - - part.on('end', function() { - self.emit('field', part.name, value); - }); - return; - } - - this._flushing++; - - var file = new File({ - path: this._uploadPath(part.filename), - name: part.filename, - type: part.mime, - hash: self.hash - }); - - this.emit('fileBegin', part.name, file); - - file.open(); - - part.on('data', function(buffer) { - self.pause(); - file.write(buffer, function() { - self.resume(); - }); - }); - - part.on('end', function() { - file.end(function() { - self._flushing--; - self.emit('file', part.name, file); - self._maybeEnd(); - }); - }); -}; - -IncomingForm.prototype._parseContentType = function() { - if (!this.headers['content-type']) { - this._error(new Error('bad content-type header, no content-type')); - return; - } - - if (this.headers['content-type'].match(/urlencoded/i)) { - this._initUrlencoded(); - return; - } - - if (this.headers['content-type'].match(/multipart/i)) { - var m; - if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { - this._initMultipart(m[1] || m[2]); - } else { - this._error(new Error('bad content-type header, no multipart boundary')); - } - return; - } - - this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); -}; - -IncomingForm.prototype._error = function(err) { - if (this.error) { - return; - } - - this.error = err; - this.pause(); - this.emit('error', err); -}; - -IncomingForm.prototype._parseContentLength = function() { - if (this.headers['content-length']) { - this.bytesReceived = 0; - this.bytesExpected = parseInt(this.headers['content-length'], 10); - this.emit('progress', this.bytesReceived, this.bytesExpected); - } -}; - -IncomingForm.prototype._newParser = function() { - return new MultipartParser(); -}; - -IncomingForm.prototype._initMultipart = function(boundary) { - this.type = 'multipart'; - - var parser = new MultipartParser(), - self = this, - headerField, - headerValue, - part; - - parser.initWithBoundary(boundary); - - parser.onPartBegin = function() { - part = new Stream(); - part.readable = true; - part.headers = {}; - part.name = null; - part.filename = null; - part.mime = null; - headerField = ''; - headerValue = ''; - }; - - parser.onHeaderField = function(b, start, end) { - headerField += b.toString(self.encoding, start, end); - }; - - parser.onHeaderValue = function(b, start, end) { - headerValue += b.toString(self.encoding, start, end); - }; - - parser.onHeaderEnd = function() { - headerField = headerField.toLowerCase(); - part.headers[headerField] = headerValue; - - var m; - if (headerField == 'content-disposition') { - if (m = headerValue.match(/name="([^"]+)"/i)) { - part.name = m[1]; - } - - part.filename = self._fileName(headerValue); - } else if (headerField == 'content-type') { - part.mime = headerValue; - } - - headerField = ''; - headerValue = ''; - }; - - parser.onHeadersEnd = function() { - self.onPart(part); - }; - - parser.onPartData = function(b, start, end) { - part.emit('data', b.slice(start, end)); - }; - - parser.onPartEnd = function() { - part.emit('end'); - }; - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._fileName = function(headerValue) { - var m = headerValue.match(/filename="(.*?)"($|; )/i) - if (!m) return; - - var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); - filename = filename.replace(/%22/g, '"'); - filename = filename.replace(/&#([\d]{4});/g, function(m, code) { - return String.fromCharCode(code); - }); - return filename; -}; - -IncomingForm.prototype._initUrlencoded = function() { - this.type = 'urlencoded'; - - var parser = new QuerystringParser() - , self = this; - - parser.onField = function(key, val) { - self.emit('field', key, val); - }; - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._uploadPath = function(filename) { - var name = ''; - for (var i = 0; i < 32; i++) { - name += Math.floor(Math.random() * 16).toString(16); - } - - if (this.keepExtensions) { - var ext = path.extname(filename); - ext = ext.replace(/(\.[a-z0-9]+).*/, '$1') - - name += ext; - } - - return path.join(this.uploadDir, name); -}; - -IncomingForm.prototype._maybeEnd = function() { - if (!this.ended || this._flushing) { - return; - } - - this.emit('end'); -}; +if (global.GENTLY) require = GENTLY.hijack(require); + +var fs = require('fs'); +var util = require('util'), + path = require('path'), + File = require('./file'), + MultipartParser = require('./multipart_parser').MultipartParser, + QuerystringParser = require('./querystring_parser').QuerystringParser, + OctetParser = require('./octet_parser').OctetParser, + JSONParser = require('./json_parser').JSONParser, + StringDecoder = require('string_decoder').StringDecoder, + EventEmitter = require('events').EventEmitter, + Stream = require('stream').Stream, + os = require('os'); + +function IncomingForm(opts) { + if (!(this instanceof IncomingForm)) return new IncomingForm(opts); + EventEmitter.call(this); + + opts=opts||{}; + + this.error = null; + this.ended = false; + + this.maxFields = opts.maxFields || 1000; + this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024; + this.keepExtensions = opts.keepExtensions || false; + this.uploadDir = opts.uploadDir || os.tmpDir(); + this.encoding = opts.encoding || 'utf-8'; + this.headers = null; + this.type = null; + this.hash = false; + + this.bytesReceived = null; + this.bytesExpected = null; + + this._parser = null; + this._flushing = 0; + this._fieldsSize = 0; + this.openedFiles = []; + + return this; +}; +util.inherits(IncomingForm, EventEmitter); +exports.IncomingForm = IncomingForm; + +IncomingForm.prototype.parse = function(req, cb) { + this.pause = function() { + try { + req.pause(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + return true; + }; + + this.resume = function() { + try { + req.resume(); + } catch (err) { + // the stream was destroyed + if (!this.ended) { + // before it was completed, crash & burn + this._error(err); + } + return false; + } + + return true; + }; + + var self = this; + req + .on('error', function(err) { + self._error(err); + }) + .on('aborted', function() { + self.emit('aborted'); + self._error(new Error('Request aborted')); + }) + .on('data', function(buffer) { + self.write(buffer); + }) + .on('end', function() { + if (self.error) { + return; + } + + var err = self._parser.end(); + if (err) { + self._error(err); + } + }); + + if (cb) { + var fields = {}, files = {}; + this + .on('field', function(name, value) { + fields[name] = value; + }) + .on('file', function(name, file) { + files[name] = file; + }) + .on('error', function(err) { + cb(err, fields, files); + }) + .on('end', function() { + cb(null, fields, files); + }); + } + + this.writeHeaders(req.headers); + + return this; +}; + +IncomingForm.prototype.writeHeaders = function(headers) { + this.headers = headers; + this._parseContentLength(); + this._parseContentType(); +}; + +IncomingForm.prototype.write = function(buffer) { + if (!this._parser) { + this._error(new Error('unintialized parser')); + return; + } + + this.bytesReceived += buffer.length; + this.emit('progress', this.bytesReceived, this.bytesExpected); + + var bytesParsed = this._parser.write(buffer); + if (bytesParsed !== buffer.length) { + this._error(new Error('parser error, '+bytesParsed+' of '+buffer.length+' bytes parsed')); + } + + return bytesParsed; +}; + +IncomingForm.prototype.pause = function() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; +}; + +IncomingForm.prototype.resume = function() { + // this does nothing, unless overwritten in IncomingForm.parse + return false; +}; + +IncomingForm.prototype.onPart = function(part) { + // this method can be overwritten by the user + this.handlePart(part); +}; + +IncomingForm.prototype.handlePart = function(part) { + var self = this; + + if (part.filename === undefined) { + var value = '' + , decoder = new StringDecoder(this.encoding); + + part.on('data', function(buffer) { + self._fieldsSize += buffer.length; + if (self._fieldsSize > self.maxFieldsSize) { + self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); + return; + } + value += decoder.write(buffer); + }); + + part.on('end', function() { + self.emit('field', part.name, value); + }); + return; + } + + this._flushing++; + + var file = new File({ + path: this._uploadPath(part.filename), + name: part.filename, + type: part.mime, + hash: self.hash + }); + + this.emit('fileBegin', part.name, file); + + file.open(); + this.openedFiles.push(file); + + part.on('data', function(buffer) { + self.pause(); + file.write(buffer, function() { + self.resume(); + }); + }); + + part.on('end', function() { + file.end(function() { + self._flushing--; + self.emit('file', part.name, file); + self._maybeEnd(); + }); + }); +}; + +function dummyParser(self) { + return { + end: function () { + self.ended = true; + self._maybeEnd(); + return null; + } + }; +} + +IncomingForm.prototype._parseContentType = function() { + if (this.bytesExpected === 0) { + this._parser = dummyParser(this); + return; + } + + if (!this.headers['content-type']) { + this._error(new Error('bad content-type header, no content-type')); + return; + } + + if (this.headers['content-type'].match(/octet-stream/i)) { + this._initOctetStream(); + return; + } + + if (this.headers['content-type'].match(/urlencoded/i)) { + this._initUrlencoded(); + return; + } + + if (this.headers['content-type'].match(/multipart/i)) { + var m; + if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { + this._initMultipart(m[1] || m[2]); + } else { + this._error(new Error('bad content-type header, no multipart boundary')); + } + return; + } + + if (this.headers['content-type'].match(/json/i)) { + this._initJSONencoded(); + return; + } + + this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); +}; + +IncomingForm.prototype._error = function(err) { + if (this.error || this.ended) { + return; + } + + this.error = err; + this.pause(); + this.emit('error', err); + + if (Array.isArray(this.openedFiles)) { + this.openedFiles.forEach(function(file) { + file._writeStream.destroy(); + setTimeout(fs.unlink, 0, file.path); + }); + } +}; + +IncomingForm.prototype._parseContentLength = function() { + this.bytesReceived = 0; + if (this.headers['content-length']) { + this.bytesExpected = parseInt(this.headers['content-length'], 10); + } else if (this.headers['transfer-encoding'] === undefined) { + this.bytesExpected = 0; + } + + if (this.bytesExpected !== null) { + this.emit('progress', this.bytesReceived, this.bytesExpected); + } +}; + +IncomingForm.prototype._newParser = function() { + return new MultipartParser(); +}; + +IncomingForm.prototype._initMultipart = function(boundary) { + this.type = 'multipart'; + + var parser = new MultipartParser(), + self = this, + headerField, + headerValue, + part; + + parser.initWithBoundary(boundary); + + parser.onPartBegin = function() { + part = new Stream(); + part.readable = true; + part.headers = {}; + part.name = null; + part.filename = null; + part.mime = null; + + part.transferEncoding = 'binary'; + part.transferBuffer = ''; + + headerField = ''; + headerValue = ''; + }; + + parser.onHeaderField = function(b, start, end) { + headerField += b.toString(self.encoding, start, end); + }; + + parser.onHeaderValue = function(b, start, end) { + headerValue += b.toString(self.encoding, start, end); + }; + + parser.onHeaderEnd = function() { + headerField = headerField.toLowerCase(); + part.headers[headerField] = headerValue; + + var m; + if (headerField == 'content-disposition') { + if (m = headerValue.match(/\bname="([^"]+)"/i)) { + part.name = m[1]; + } + + part.filename = self._fileName(headerValue); + } else if (headerField == 'content-type') { + part.mime = headerValue; + } else if (headerField == 'content-transfer-encoding') { + part.transferEncoding = headerValue.toLowerCase(); + } + + headerField = ''; + headerValue = ''; + }; + + parser.onHeadersEnd = function() { + switch(part.transferEncoding){ + case 'binary': + case '7bit': + case '8bit': + parser.onPartData = function(b, start, end) { + part.emit('data', b.slice(start, end)); + }; + + parser.onPartEnd = function() { + part.emit('end'); + }; + break; + + case 'base64': + parser.onPartData = function(b, start, end) { + part.transferBuffer += b.slice(start, end).toString('ascii'); + + /* + four bytes (chars) in base64 converts to three bytes in binary + encoding. So we should always work with a number of bytes that + can be divided by 4, it will result in a number of buytes that + can be divided vy 3. + */ + var offset = parseInt(part.transferBuffer.length / 4) * 4; + part.emit('data', new Buffer(part.transferBuffer.substring(0, offset), 'base64')) + part.transferBuffer = part.transferBuffer.substring(offset); + }; + + parser.onPartEnd = function() { + part.emit('data', new Buffer(part.transferBuffer, 'base64')) + part.emit('end'); + }; + break; + + default: + return self._error(new Error('unknown transfer-encoding')); + } + + self.onPart(part); + }; + + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._fileName = function(headerValue) { + var m = headerValue.match(/\bfilename="(.*?)"($|; )/i); + if (!m) return; + + var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); + filename = filename.replace(/%22/g, '"'); + filename = filename.replace(/&#([\d]{4});/g, function(m, code) { + return String.fromCharCode(code); + }); + return filename; +}; + +IncomingForm.prototype._initUrlencoded = function() { + this.type = 'urlencoded'; + + var parser = new QuerystringParser(this.maxFields) + , self = this; + + parser.onField = function(key, val) { + self.emit('field', key, val); + }; + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._initOctetStream = function() { + this.type = 'octet-stream'; + var filename = this.headers['x-file-name']; + var mime = this.headers['content-type']; + + var file = new File({ + path: this._uploadPath(filename), + name: filename, + type: mime + }); + + file.open(); + + this.emit('fileBegin', filename, file); + + this._flushing++; + + var self = this; + + self._parser = new OctetParser(); + + //Keep track of writes that haven't finished so we don't emit the file before it's done being written + var outstandingWrites = 0; + + self._parser.on('data', function(buffer){ + self.pause(); + outstandingWrites++; + + file.write(buffer, function() { + outstandingWrites--; + self.resume(); + + if(self.ended){ + self._parser.emit('doneWritingFile'); + } + }); + }); + + self._parser.on('end', function(){ + self._flushing--; + self.ended = true; + + var done = function(){ + self.emit('file', 'file', file); + self._maybeEnd(); + }; + + if(outstandingWrites === 0){ + done(); + } else { + self._parser.once('doneWritingFile', done); + } + }); +}; + +IncomingForm.prototype._initJSONencoded = function() { + this.type = 'json'; + + var parser = new JSONParser() + , self = this; + + if (this.bytesExpected) { + parser.initWithLength(this.bytesExpected); + } + + parser.onField = function(key, val) { + self.emit('field', key, val); + } + + parser.onEnd = function() { + self.ended = true; + self._maybeEnd(); + }; + + this._parser = parser; +}; + +IncomingForm.prototype._uploadPath = function(filename) { + var name = ''; + for (var i = 0; i < 32; i++) { + name += Math.floor(Math.random() * 16).toString(16); + } + + if (this.keepExtensions) { + var ext = path.extname(filename); + ext = ext.replace(/(\.[a-z0-9]+).*/, '$1'); + + name += ext; + } + + return path.join(this.uploadDir, name); +}; + +IncomingForm.prototype._maybeEnd = function() { + if (!this.ended || this._flushing || this.error) { + return; + } + + this.emit('end'); +}; + diff --git a/websvr/lib/json_parser.js b/websvr/lib/json_parser.js new file mode 100644 index 0000000..6ce966b --- /dev/null +++ b/websvr/lib/json_parser.js @@ -0,0 +1,35 @@ +if (global.GENTLY) require = GENTLY.hijack(require); + +var Buffer = require('buffer').Buffer + +function JSONParser() { + this.data = new Buffer(''); + this.bytesWritten = 0; +}; +exports.JSONParser = JSONParser; + +JSONParser.prototype.initWithLength = function(length) { + this.data = new Buffer(length); +} + +JSONParser.prototype.write = function(buffer) { + if (this.data.length >= this.bytesWritten + buffer.length) { + buffer.copy(this.data, this.bytesWritten); + } else { + this.data = Buffer.concat([this.data, buffer]); + } + this.bytesWritten += buffer.length; + return buffer.length; +} + +JSONParser.prototype.end = function() { + try { + var fields = JSON.parse(this.data.toString('utf8')) + for (var field in fields) { + this.onField(field, fields[field]); + } + } catch (e) {} + this.data = null; + + this.onEnd(); +} \ No newline at end of file diff --git a/websvr/lib/mime.js b/websvr/lib/mime.js index 28dcbfe..8a7eb09 100644 --- a/websvr/lib/mime.js +++ b/websvr/lib/mime.js @@ -1,93 +1,114 @@ -var path = require('path'), - fs = require('fs'); - -var mime = module.exports = { - // Map of extension to mime type - types: Object.create(null), - - // Map of mime type to extension - extensions :Object.create(null), - - /** - * Define mimetype -> extension mappings. Each key is a mime-type that maps - * to an array of extensions associated with the type. The first extension is - * used as the default extension for the type. - * - * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); - * - * @param map (Object) type definitions - */ - define: function(map) { - for (var type in map) { - var exts = map[type]; - - for (var i = 0; i < exts.length; i++) { - mime.types[exts[i]] = type; - } - - // Default extension is the first one we encounter - if (!mime.extensions[type]) { - mime.extensions[type] = exts[0]; - } - } - }, - - /** - * Load an Apache2-style ".types" file - * - * This may be called multiple times (it's expected). Where files declare - * overlapping types/extensions, the last file wins. - * - * @param file (String) path of file to load. - */ - load: function(file) { - // Read file and split into lines - var map = {}, - content = fs.readFileSync(file, 'ascii'), - lines = content.split(/[\r\n]+/); - - lines.forEach(function(line, lineno) { - // Clean up whitespace/comments, and split into fields - var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); - map[fields.shift()] = fields; - }); - - mime.define(map); - }, - - /** - * Lookup a mime type based on extension - */ - lookup: function(path, fallback) { - var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); - - return mime.types[ext] || fallback || mime.default_type - }, - - /** - * Return file extension associated with a mime type - */ - extension: function(mimeType) { - return mime.extensions[mimeType]; - }, - - /** - * Lookup a charset based on mime type. - */ - charsets: { - lookup: function (mimeType, fallback) { - // Assume text types are utf8. Modify mime logic as needed. - return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; - } - } -}; - -// Load our local copy of -// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -mime.load(path.join(__dirname, 'types/mime.types')); - -// Overlay enhancements submitted by the node.js community -mime.load(path.join(__dirname, 'types/node.types')); - -// Set the default type -mime.default_type = mime.types.bin; +var path = require('path'); +var fs = require('fs'); + +function Mime() { + // Map of extension -> mime type + this.types = Object.create(null); + + // Map of mime type -> extension + this.extensions = Object.create(null); +} + +/** + * Define mimetype -> extension mappings. Each key is a mime-type that maps + * to an array of extensions associated with the type. The first extension is + * used as the default extension for the type. + * + * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); + * + * @param map (Object) type definitions + */ +Mime.prototype.define = function (map) { + for (var type in map) { + var exts = map[type]; + + for (var i = 0; i < exts.length; i++) { + if (process.env.DEBUG_MIME && this.types[exts]) { + console.warn(this._loading.replace(/.*\//, ''), 'changes "' + exts[i] + '" extension type from ' + + this.types[exts] + ' to ' + type); + } + + this.types[exts[i]] = type; + } + + // Default extension is the first one we encounter + if (!this.extensions[type]) { + this.extensions[type] = exts[0]; + } + } +}; + +/** + * Load an Apache2-style ".types" file + * + * This may be called multiple times (it's expected). Where files declare + * overlapping types/extensions, the last file wins. + * + * @param file (String) path of file to load. + */ +Mime.prototype.load = function(file) { + + this._loading = file; + // Read file and split into lines + var map = {}, + content = fs.readFileSync(file, 'ascii'), + lines = content.split(/[\r\n]+/); + + lines.forEach(function(line) { + // Clean up whitespace/comments, and split into fields + var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); + map[fields.shift()] = fields; + }); + + this.define(map); + + this._loading = null; +}; + +/** + * Lookup a mime type based on extension + */ +Mime.prototype.lookup = function(path, fallback) { + var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); + + return this.types[ext] || fallback || this.default_type; +}; + +/** + * Return file extension associated with a mime type + */ +Mime.prototype.extension = function(mimeType) { + var type = mimeType.match(/^\s*([^;\s]*)(?:;|\s|$)/)[1].toLowerCase(); + return this.extensions[type]; +}; + +// Default instance +var mime = new Mime(); + +// Load local copy of +// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types +mime.load(path.join(__dirname, 'types/mime.types')); + +// Load additional types from node.js community +mime.load(path.join(__dirname, 'types/node.types')); + +// Default type +mime.default_type = mime.lookup('bin'); + +// +// Additional API specific to the default instance +// + +mime.Mime = Mime; + +/** + * Lookup a charset based on mime type. + */ +mime.charsets = { + lookup: function(mimeType, fallback) { + // Assume text types are utf8 + return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; + } +}; + +module.exports = mime; diff --git a/websvr/lib/multipart_parser.js b/websvr/lib/multipart_parser.js index f73140c..98a6856 100644 --- a/websvr/lib/multipart_parser.js +++ b/websvr/lib/multipart_parser.js @@ -1,312 +1,324 @@ -var Buffer = require('buffer').Buffer, - s = 0, - S = - { PARSER_UNINITIALIZED: 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, - Z = 122, - - lower = function(c) { - return c | 0x20; - }; - -for (var s in S) { - exports[s] = S[s]; -} - -function MultipartParser() { - this.boundary = null; - this.boundaryChars = null; - this.lookbehind = null; - this.state = S.PARSER_UNINITIALIZED; - - this.index = null; - this.flags = 0; -}; -exports.MultipartParser = MultipartParser; - -MultipartParser.stateToString = function(stateNumber) { - for (var state in S) { - var number = S[state]; - if (number === stateNumber) return state; - } -}; - -MultipartParser.prototype.initWithBoundary = function(str) { - this.boundary = new Buffer(str.length+4); - this.boundary.write('\r\n--', 'ascii', 0); - this.boundary.write(str, 'ascii', 4); - this.lookbehind = new Buffer(this.boundary.length+8); - this.state = S.START; - - this.boundaryChars = {}; - for (var i = 0; i < this.boundary.length; i++) { - this.boundaryChars[this.boundary[i]] = true; - } -}; - -MultipartParser.prototype.write = function(buffer) { - var self = this, - i = 0, - len = buffer.length, - prevIndex = this.index, - index = this.index, - state = this.state, - flags = this.flags, - lookbehind = this.lookbehind, - boundary = this.boundary, - boundaryChars = this.boundaryChars, - boundaryLength = this.boundary.length, - boundaryEnd = boundaryLength - 1, - bufferLength = buffer.length, - c, - cl, - - mark = function(name) { - self[name+'Mark'] = i; - }, - clear = function(name) { - delete self[name+'Mark']; - }, - callback = function(name, buffer, start, end) { - if (start !== undefined && start === end) { - return; - } - - var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); - if (callbackSymbol in self) { - self[callbackSymbol](buffer, start, end); - } - }, - dataCallback = function(name, clear) { - var markSymbol = name+'Mark'; - if (!(markSymbol in self)) { - return; - } - - if (!clear) { - callback(name, buffer, self[markSymbol], buffer.length); - self[markSymbol] = 0; - } else { - callback(name, buffer, self[markSymbol], i); - delete self[markSymbol]; - } - }; - - for (i = 0; i < len; i++) { - c = buffer[i]; - switch (state) { - case S.PARSER_UNINITIALIZED: - return i; - case S.START: - index = 0; - state = S.START_BOUNDARY; - case S.START_BOUNDARY: - if (index == boundary.length - 2) { - if (c != CR) { - return i; - } - index++; - break; - } else if (index - 1 == boundary.length - 2) { - if (c != LF) { - return i; - } - index = 0; - 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; - mark('headerField'); - index = 0; - case S.HEADER_FIELD: - if (c == CR) { - clear('headerField'); - state = S.HEADERS_ALMOST_DONE; - break; - } - - index++; - if (c == HYPHEN) { - break; - } - - if (c == COLON) { - if (index == 1) { - // empty header field - return i; - } - dataCallback('headerField', true); - state = S.HEADER_VALUE_START; - break; - } - - cl = lower(c); - if (cl < A || cl > Z) { - return i; - } - break; - case S.HEADER_VALUE_START: - if (c == SPACE) { - break; - } - - mark('headerValue'); - state = S.HEADER_VALUE; - case S.HEADER_VALUE: - if (c == CR) { - dataCallback('headerValue', true); - 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; - } - - callback('headersEnd'); - state = S.PART_DATA_START; - break; - case S.PART_DATA_START: - state = S.PART_DATA - mark('partData'); - 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 < boundary.length) { - if (boundary[index] == c) { - if (index == 0) { - dataCallback('partData', true); - } - index++; - } else { - index = 0; - } - } else if (index == boundary.length) { - 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 == boundary.length) { - if (flags & F.PART_BOUNDARY) { - index = 0; - if (c == LF) { - // unset the PART_BOUNDARY flag - flags &= ~F.PART_BOUNDARY; - callback('partEnd'); - callback('partBegin'); - state = S.HEADER_FIELD_START; - break; - } - } else if (flags & F.LAST_BOUNDARY) { - if (c == HYPHEN) { - callback('partEnd'); - 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 - callback('partData', lookbehind, 0, prevIndex); - prevIndex = 0; - mark('partData'); - - // 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; - } - } - - dataCallback('headerField'); - dataCallback('headerValue'); - dataCallback('partData'); - - this.index = index; - this.state = state; - this.flags = flags; - - return len; -}; - -MultipartParser.prototype.end = function() { - if (this.state != S.END) { - return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); - } -}; - -MultipartParser.prototype.explain = function() { - return 'state = ' + MultipartParser.stateToString(this.state); -}; +var Buffer = require('buffer').Buffer, + s = 0, + S = + { PARSER_UNINITIALIZED: 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, + Z = 122, + + lower = function(c) { + return c | 0x20; + }; + +for (s in S) { + exports[s] = S[s]; +} + +function MultipartParser() { + this.boundary = null; + this.boundaryChars = null; + this.lookbehind = null; + this.state = S.PARSER_UNINITIALIZED; + + this.index = null; + this.flags = 0; +}; +exports.MultipartParser = MultipartParser; + +MultipartParser.stateToString = function(stateNumber) { + for (var state in S) { + var number = S[state]; + if (number === stateNumber) return state; + } +}; + +MultipartParser.prototype.initWithBoundary = function(str) { + this.boundary = new Buffer(str.length+4); + this.boundary.write('\r\n--', 'ascii', 0); + this.boundary.write(str, 'ascii', 4); + this.lookbehind = new Buffer(this.boundary.length+8); + this.state = S.START; + + this.boundaryChars = {}; + for (var i = 0; i < this.boundary.length; i++) { + this.boundaryChars[this.boundary[i]] = true; + } +}; + +MultipartParser.prototype.write = function(buffer) { + var self = this, + i = 0, + len = buffer.length, + prevIndex = this.index, + index = this.index, + state = this.state, + flags = this.flags, + lookbehind = this.lookbehind, + boundary = this.boundary, + boundaryChars = this.boundaryChars, + boundaryLength = this.boundary.length, + boundaryEnd = boundaryLength - 1, + bufferLength = buffer.length, + c, + cl, + + mark = function(name) { + self[name+'Mark'] = i; + }, + clear = function(name) { + delete self[name+'Mark']; + }, + callback = function(name, buffer, start, end) { + if (start !== undefined && start === end) { + return; + } + + var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); + if (callbackSymbol in self) { + self[callbackSymbol](buffer, start, end); + } + }, + dataCallback = function(name, clear) { + var markSymbol = name+'Mark'; + if (!(markSymbol in self)) { + return; + } + + if (!clear) { + callback(name, buffer, self[markSymbol], buffer.length); + self[markSymbol] = 0; + } else { + callback(name, buffer, self[markSymbol], i); + delete self[markSymbol]; + } + }; + + for (i = 0; i < len; i++) { + c = buffer[i]; + switch (state) { + case S.PARSER_UNINITIALIZED: + return i; + case S.START: + index = 0; + state = S.START_BOUNDARY; + case S.START_BOUNDARY: + if (index == boundary.length - 2) { + if (c != CR) { + return i; + } + index++; + break; + } else if (index - 1 == boundary.length - 2) { + if (c != LF) { + return i; + } + index = 0; + callback('partBegin'); + state = S.HEADER_FIELD_START; + break; + } + + if (c != boundary[index+2]) { + index = -2; + } + if (c == boundary[index+2]) { + index++; + } + break; + case S.HEADER_FIELD_START: + state = S.HEADER_FIELD; + mark('headerField'); + index = 0; + case S.HEADER_FIELD: + if (c == CR) { + clear('headerField'); + state = S.HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c == HYPHEN) { + break; + } + + if (c == COLON) { + if (index == 1) { + // empty header field + return i; + } + dataCallback('headerField', true); + state = S.HEADER_VALUE_START; + break; + } + + cl = lower(c); + if (cl < A || cl > Z) { + return i; + } + break; + case S.HEADER_VALUE_START: + if (c == SPACE) { + break; + } + + mark('headerValue'); + state = S.HEADER_VALUE; + case S.HEADER_VALUE: + if (c == CR) { + dataCallback('headerValue', true); + 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; + } + + callback('headersEnd'); + state = S.PART_DATA_START; + break; + case S.PART_DATA_START: + state = S.PART_DATA; + mark('partData'); + 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 < boundary.length) { + if (boundary[index] == c) { + if (index == 0) { + dataCallback('partData', true); + } + index++; + } else { + index = 0; + } + } else if (index == boundary.length) { + 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 == boundary.length) { + if (flags & F.PART_BOUNDARY) { + index = 0; + if (c == LF) { + // unset the PART_BOUNDARY flag + flags &= ~F.PART_BOUNDARY; + callback('partEnd'); + callback('partBegin'); + state = S.HEADER_FIELD_START; + break; + } + } else if (flags & F.LAST_BOUNDARY) { + if (c == HYPHEN) { + callback('partEnd'); + 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 + callback('partData', lookbehind, 0, prevIndex); + prevIndex = 0; + mark('partData'); + + // 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; + } + } + + dataCallback('headerField'); + dataCallback('headerValue'); + dataCallback('partData'); + + this.index = index; + this.state = state; + this.flags = flags; + + return len; +}; + +MultipartParser.prototype.end = function() { + var callback = function(self, name) { + var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); + if (callbackSymbol in self) { + self[callbackSymbol](); + } + }; + if ((this.state == S.HEADER_FIELD_START && this.index == 0) || + (this.state == S.PART_DATA && this.index == this.boundary.length)) { + callback(this, 'partEnd'); + callback(this, 'end'); + } else if (this.state != S.END) { + return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); + } +}; + +MultipartParser.prototype.explain = function() { + return 'state = ' + MultipartParser.stateToString(this.state); +}; diff --git a/websvr/lib/octet_parser.js b/websvr/lib/octet_parser.js new file mode 100644 index 0000000..6e8b551 --- /dev/null +++ b/websvr/lib/octet_parser.js @@ -0,0 +1,20 @@ +var EventEmitter = require('events').EventEmitter + , util = require('util'); + +function OctetParser(options){ + if(!(this instanceof OctetParser)) return new OctetParser(options); + EventEmitter.call(this); +} + +util.inherits(OctetParser, EventEmitter); + +exports.OctetParser = OctetParser; + +OctetParser.prototype.write = function(buffer) { + this.emit('data', buffer); + return buffer.length; +}; + +OctetParser.prototype.end = function() { + this.emit('end'); +}; diff --git a/websvr/lib/querystring_parser.js b/websvr/lib/querystring_parser.js index b8e86e3..320ce5a 100644 --- a/websvr/lib/querystring_parser.js +++ b/websvr/lib/querystring_parser.js @@ -1,25 +1,27 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -// This is a buffering parser, not quite as nice as the multipart one. -// If I find time I'll rewrite this to be fully streaming as well -var querystring = require('querystring'); - -function QuerystringParser() { - this.buffer = ''; -}; -exports.QuerystringParser = QuerystringParser; - -QuerystringParser.prototype.write = function(buffer) { - this.buffer += buffer.toString('ascii'); - return buffer.length; -}; - -QuerystringParser.prototype.end = function() { - var fields = querystring.parse(this.buffer); - for (var field in fields) { - this.onField(field, fields[field]); - } - this.buffer = ''; - - this.onEnd(); -}; \ No newline at end of file +if (global.GENTLY) require = GENTLY.hijack(require); + +// This is a buffering parser, not quite as nice as the multipart one. +// If I find time I'll rewrite this to be fully streaming as well +var querystring = require('querystring'); + +function QuerystringParser(maxKeys) { + this.maxKeys = maxKeys; + this.buffer = ''; +}; +exports.QuerystringParser = QuerystringParser; + +QuerystringParser.prototype.write = function(buffer) { + this.buffer += buffer.toString('ascii'); + return buffer.length; +}; + +QuerystringParser.prototype.end = function() { + var fields = querystring.parse(this.buffer, '&', '=', { maxKeys: this.maxKeys }); + for (var field in fields) { + this.onField(field, fields[field]); + } + this.buffer = ''; + + this.onEnd(); +}; + diff --git a/websvr/lib/types/mime.types b/websvr/lib/types/mime.types index cf9dbe8..b90b165 100644 --- a/websvr/lib/types/mime.types +++ b/websvr/lib/types/mime.types @@ -1,1510 +1,1588 @@ -# This file maps Internet media types to unique file extension(s). -# Although created for httpd, this file is used by many software systems -# and has been placed in the public domain for unlimited redisribution. -# -# The table below contains both registered and (common) unregistered types. -# A type that has no unique extension can be ignored -- they are listed -# here to guide configurations toward known types and to make it easier to -# identify "new" types. File extensions are also commonly used to indicate -# content languages and encodings, so choose them carefully. -# -# Internet media types should be registered as described in RFC 4288. -# The registry is at . -# -# MIME type (lowercased) Extensions -# ============================================ ========== -# application/1d-interleaved-parityfec -# application/3gpp-ims+xml -# application/activemessage -application/andrew-inset ez -# application/applefile -application/applixware aw -application/atom+xml atom -application/atomcat+xml atomcat -# application/atomicmail -application/atomsvc+xml atomsvc -# application/auth-policy+xml -# application/batch-smtp -# application/beep+xml -# application/calendar+xml -# application/cals-1840 -# application/ccmp+xml -application/ccxml+xml ccxml -application/cdmi-capability cdmia -application/cdmi-container cdmic -application/cdmi-domain cdmid -application/cdmi-object cdmio -application/cdmi-queue cdmiq -# application/cea-2018+xml -# application/cellml+xml -# application/cfw -# application/cnrp+xml -# application/commonground -# application/conference-info+xml -# application/cpl+xml -# application/csta+xml -# application/cstadata+xml -application/cu-seeme cu -# application/cybercash -application/davmount+xml davmount -# application/dca-rft -# application/dec-dx -# application/dialog-info+xml -# application/dicom -# application/dns -# application/dskpp+xml -application/dssc+der dssc -application/dssc+xml xdssc -# application/dvcs -application/ecmascript ecma -# application/edi-consent -# application/edi-x12 -# application/edifact -application/emma+xml emma -# application/epp+xml -application/epub+zip epub -# application/eshop -# application/example -application/exi exi -# application/fastinfoset -# application/fastsoap -# application/fits -application/font-tdpfr pfr -# application/framework-attributes+xml -# application/h224 -# application/held+xml -# application/http -application/hyperstudio stk -# application/ibe-key-request+xml -# application/ibe-pkg-reply+xml -# application/ibe-pp-data -# application/iges -# application/im-iscomposing+xml -# application/index -# application/index.cmd -# application/index.obj -# application/index.response -# application/index.vnd -application/inkml+xml ink inkml -# application/iotp -application/ipfix ipfix -# application/ipp -# application/isup -application/java-archive jar -application/java-serialized-object ser -application/java-vm class -application/javascript js -application/json json -# application/kpml-request+xml -# application/kpml-response+xml -application/lost+xml lostxml -application/mac-binhex40 hqx -application/mac-compactpro cpt -# application/macwriteii -application/mads+xml mads -application/marc mrc -application/marcxml+xml mrcx -application/mathematica ma nb mb -# application/mathml-content+xml -# application/mathml-presentation+xml -application/mathml+xml mathml -# application/mbms-associated-procedure-description+xml -# application/mbms-deregister+xml -# application/mbms-envelope+xml -# application/mbms-msk+xml -# application/mbms-msk-response+xml -# application/mbms-protection-description+xml -# application/mbms-reception-report+xml -# application/mbms-register+xml -# application/mbms-register-response+xml -# application/mbms-user-service-description+xml -application/mbox mbox -# application/media_control+xml -application/mediaservercontrol+xml mscml -application/metalink4+xml meta4 -application/mets+xml mets -# application/mikey -application/mods+xml mods -# application/moss-keys -# application/moss-signature -# application/mosskey-data -# application/mosskey-request -application/mp21 m21 mp21 -application/mp4 mp4s -# application/mpeg4-generic -# application/mpeg4-iod -# application/mpeg4-iod-xmt -# application/msc-ivr+xml -# application/msc-mixer+xml -application/msword doc dot -application/mxf mxf -# application/nasdata -# application/news-checkgroups -# application/news-groupinfo -# application/news-transmission -# application/nss -# application/ocsp-request -# application/ocsp-response -application/octet-stream bin dms lha lrf lzh so iso dmg dist distz pkg bpk dump elc deploy -application/oda oda -application/oebps-package+xml opf -application/ogg ogx -application/onenote onetoc onetoc2 onetmp onepkg -application/oxps oxps -# application/parityfec -application/patch-ops-error+xml xer -application/pdf pdf -application/pgp-encrypted pgp -# application/pgp-keys -application/pgp-signature asc sig -application/pics-rules prf -# application/pidf+xml -# application/pidf-diff+xml -application/pkcs10 p10 -application/pkcs7-mime p7m p7c -application/pkcs7-signature p7s -application/pkcs8 p8 -application/pkix-attr-cert ac -application/pkix-cert cer -application/pkix-crl crl -application/pkix-pkipath pkipath -application/pkixcmp pki -application/pls+xml pls -# application/poc-settings+xml -application/postscript ai eps ps -# application/prs.alvestrand.titrax-sheet -application/prs.cww cww -# application/prs.nprend -# application/prs.plucker -# application/prs.rdf-xml-crypt -# application/prs.xsf+xml -application/pskc+xml pskcxml -# application/qsig -application/rdf+xml rdf -application/reginfo+xml rif -application/relax-ng-compact-syntax rnc -# application/remote-printing -application/resource-lists+xml rl -application/resource-lists-diff+xml rld -# application/riscos -# application/rlmi+xml -application/rls-services+xml rs -application/rpki-ghostbusters gbr -application/rpki-manifest mft -application/rpki-roa roa -# application/rpki-updown -application/rsd+xml rsd -application/rss+xml rss -application/rtf rtf -# application/rtx -# application/samlassertion+xml -# application/samlmetadata+xml -application/sbml+xml sbml -application/scvp-cv-request scq -application/scvp-cv-response scs -application/scvp-vp-request spq -application/scvp-vp-response spp -application/sdp sdp -# application/set-payment -application/set-payment-initiation setpay -# application/set-registration -application/set-registration-initiation setreg -# application/sgml -# application/sgml-open-catalog -application/shf+xml shf -# application/sieve -# application/simple-filter+xml -# application/simple-message-summary -# application/simplesymbolcontainer -# application/slate -# application/smil -application/smil+xml smi smil -# application/soap+fastinfoset -# application/soap+xml -application/sparql-query rq -application/sparql-results+xml srx -# application/spirits-event+xml -application/srgs gram -application/srgs+xml grxml -application/sru+xml sru -application/ssml+xml ssml -# application/tamp-apex-update -# application/tamp-apex-update-confirm -# application/tamp-community-update -# application/tamp-community-update-confirm -# application/tamp-error -# application/tamp-sequence-adjust -# application/tamp-sequence-adjust-confirm -# application/tamp-status-query -# application/tamp-status-response -# application/tamp-update -# application/tamp-update-confirm -application/tei+xml tei teicorpus -application/thraud+xml tfi -# application/timestamp-query -# application/timestamp-reply -application/timestamped-data tsd -# application/tve-trigger -# application/ulpfec -# application/vcard+xml -# application/vemmi -# application/vividence.scriptfile -# application/vnd.3gpp.bsf+xml -application/vnd.3gpp.pic-bw-large plb -application/vnd.3gpp.pic-bw-small psb -application/vnd.3gpp.pic-bw-var pvb -# application/vnd.3gpp.sms -# application/vnd.3gpp2.bcmcsinfo+xml -# application/vnd.3gpp2.sms -application/vnd.3gpp2.tcap tcap -application/vnd.3m.post-it-notes pwn -application/vnd.accpac.simply.aso aso -application/vnd.accpac.simply.imp imp -application/vnd.acucobol acu -application/vnd.acucorp atc acutc -application/vnd.adobe.air-application-installer-package+zip air -application/vnd.adobe.fxp fxp fxpl -# application/vnd.adobe.partial-upload -application/vnd.adobe.xdp+xml xdp -application/vnd.adobe.xfdf xfdf -# application/vnd.aether.imp -# application/vnd.ah-barcode -application/vnd.ahead.space ahead -application/vnd.airzip.filesecure.azf azf -application/vnd.airzip.filesecure.azs azs -application/vnd.amazon.ebook azw -application/vnd.americandynamics.acc acc -application/vnd.amiga.ami ami -# application/vnd.amundsen.maze+xml -application/vnd.android.package-archive apk -application/vnd.anser-web-certificate-issue-initiation cii -application/vnd.anser-web-funds-transfer-initiation fti -application/vnd.antix.game-component atx -application/vnd.apple.installer+xml mpkg -application/vnd.apple.mpegurl m3u8 -# application/vnd.arastra.swi -application/vnd.aristanetworks.swi swi -application/vnd.astraea-software.iota iota -application/vnd.audiograph aep -# application/vnd.autopackage -# application/vnd.avistar+xml -application/vnd.blueice.multipass mpm -# application/vnd.bluetooth.ep.oob -application/vnd.bmi bmi -application/vnd.businessobjects rep -# application/vnd.cab-jscript -# application/vnd.canon-cpdl -# application/vnd.canon-lips -# application/vnd.cendio.thinlinc.clientconf -application/vnd.chemdraw+xml cdxml -application/vnd.chipnuts.karaoke-mmd mmd -application/vnd.cinderella cdy -# application/vnd.cirpack.isdn-ext -application/vnd.claymore cla -application/vnd.cloanto.rp9 rp9 -application/vnd.clonk.c4group c4g c4d c4f c4p c4u -application/vnd.cluetrust.cartomobile-config c11amc -application/vnd.cluetrust.cartomobile-config-pkg c11amz -# application/vnd.collection+json -# application/vnd.commerce-battelle -application/vnd.commonspace csp -application/vnd.contact.cmsg cdbcmsg -application/vnd.cosmocaller cmc -application/vnd.crick.clicker clkx -application/vnd.crick.clicker.keyboard clkk -application/vnd.crick.clicker.palette clkp -application/vnd.crick.clicker.template clkt -application/vnd.crick.clicker.wordbank clkw -application/vnd.criticaltools.wbs+xml wbs -application/vnd.ctc-posml pml -# application/vnd.ctct.ws+xml -# application/vnd.cups-pdf -# application/vnd.cups-postscript -application/vnd.cups-ppd ppd -# application/vnd.cups-raster -# application/vnd.cups-raw -# application/vnd.curl -application/vnd.curl.car car -application/vnd.curl.pcurl pcurl -# application/vnd.cybank -application/vnd.data-vision.rdz rdz -application/vnd.dece.data uvf uvvf uvd uvvd -application/vnd.dece.ttml+xml uvt uvvt -application/vnd.dece.unspecified uvx uvvx -application/vnd.dece.zip uvz uvvz -application/vnd.denovo.fcselayout-link fe_launch -# application/vnd.dir-bi.plate-dl-nosuffix -application/vnd.dna dna -application/vnd.dolby.mlp mlp -# application/vnd.dolby.mobile.1 -# application/vnd.dolby.mobile.2 -application/vnd.dpgraph dpg -application/vnd.dreamfactory dfac -application/vnd.dvb.ait ait -# application/vnd.dvb.dvbj -# application/vnd.dvb.esgcontainer -# application/vnd.dvb.ipdcdftnotifaccess -# application/vnd.dvb.ipdcesgaccess -# application/vnd.dvb.ipdcesgaccess2 -# application/vnd.dvb.ipdcesgpdd -# application/vnd.dvb.ipdcroaming -# application/vnd.dvb.iptv.alfec-base -# application/vnd.dvb.iptv.alfec-enhancement -# application/vnd.dvb.notif-aggregate-root+xml -# application/vnd.dvb.notif-container+xml -# application/vnd.dvb.notif-generic+xml -# application/vnd.dvb.notif-ia-msglist+xml -# application/vnd.dvb.notif-ia-registration-request+xml -# application/vnd.dvb.notif-ia-registration-response+xml -# application/vnd.dvb.notif-init+xml -# application/vnd.dvb.pfr -application/vnd.dvb.service svc -# application/vnd.dxr -application/vnd.dynageo geo -# application/vnd.easykaraoke.cdgdownload -# application/vnd.ecdis-update -application/vnd.ecowin.chart mag -# application/vnd.ecowin.filerequest -# application/vnd.ecowin.fileupdate -# application/vnd.ecowin.series -# application/vnd.ecowin.seriesrequest -# application/vnd.ecowin.seriesupdate -# application/vnd.emclient.accessrequest+xml -application/vnd.enliven nml -# application/vnd.eprints.data+xml -application/vnd.epson.esf esf -application/vnd.epson.msf msf -application/vnd.epson.quickanime qam -application/vnd.epson.salt slt -application/vnd.epson.ssf ssf -# application/vnd.ericsson.quickcall -application/vnd.eszigno3+xml es3 et3 -# application/vnd.etsi.aoc+xml -# application/vnd.etsi.cug+xml -# application/vnd.etsi.iptvcommand+xml -# application/vnd.etsi.iptvdiscovery+xml -# application/vnd.etsi.iptvprofile+xml -# application/vnd.etsi.iptvsad-bc+xml -# application/vnd.etsi.iptvsad-cod+xml -# application/vnd.etsi.iptvsad-npvr+xml -# application/vnd.etsi.iptvservice+xml -# application/vnd.etsi.iptvsync+xml -# application/vnd.etsi.iptvueprofile+xml -# application/vnd.etsi.mcid+xml -# application/vnd.etsi.overload-control-policy-dataset+xml -# application/vnd.etsi.sci+xml -# application/vnd.etsi.simservs+xml -# application/vnd.etsi.tsl+xml -# application/vnd.etsi.tsl.der -# application/vnd.eudora.data -application/vnd.ezpix-album ez2 -application/vnd.ezpix-package ez3 -# application/vnd.f-secure.mobile -application/vnd.fdf fdf -application/vnd.fdsn.mseed mseed -application/vnd.fdsn.seed seed dataless -# application/vnd.ffsns -# application/vnd.fints -application/vnd.flographit gph -application/vnd.fluxtime.clip ftc -# application/vnd.font-fontforge-sfd -application/vnd.framemaker fm frame maker book -application/vnd.frogans.fnc fnc -application/vnd.frogans.ltf ltf -application/vnd.fsc.weblaunch fsc -application/vnd.fujitsu.oasys oas -application/vnd.fujitsu.oasys2 oa2 -application/vnd.fujitsu.oasys3 oa3 -application/vnd.fujitsu.oasysgp fg5 -application/vnd.fujitsu.oasysprs bh2 -# application/vnd.fujixerox.art-ex -# application/vnd.fujixerox.art4 -# application/vnd.fujixerox.hbpl -application/vnd.fujixerox.ddd ddd -application/vnd.fujixerox.docuworks xdw -application/vnd.fujixerox.docuworks.binder xbd -# application/vnd.fut-misnet -application/vnd.fuzzysheet fzs -application/vnd.genomatix.tuxedo txd -# application/vnd.geocube+xml -application/vnd.geogebra.file ggb -application/vnd.geogebra.tool ggt -application/vnd.geometry-explorer gex gre -application/vnd.geonext gxt -application/vnd.geoplan g2w -application/vnd.geospace g3w -# application/vnd.globalplatform.card-content-mgt -# application/vnd.globalplatform.card-content-mgt-response -application/vnd.gmx gmx -application/vnd.google-earth.kml+xml kml -application/vnd.google-earth.kmz kmz -application/vnd.grafeq gqf gqs -# application/vnd.gridmp -application/vnd.groove-account gac -application/vnd.groove-help ghf -application/vnd.groove-identity-message gim -application/vnd.groove-injector grv -application/vnd.groove-tool-message gtm -application/vnd.groove-tool-template tpl -application/vnd.groove-vcard vcg -# application/vnd.hal+json -application/vnd.hal+xml hal -application/vnd.handheld-entertainment+xml zmm -application/vnd.hbci hbci -# application/vnd.hcl-bireports -application/vnd.hhe.lesson-player les -application/vnd.hp-hpgl hpgl -application/vnd.hp-hpid hpid -application/vnd.hp-hps hps -application/vnd.hp-jlyt jlt -application/vnd.hp-pcl pcl -application/vnd.hp-pclxl pclxl -# application/vnd.httphone -application/vnd.hydrostatix.sof-data sfd-hdstx -application/vnd.hzn-3d-crossword x3d -# application/vnd.ibm.afplinedata -# application/vnd.ibm.electronic-media -application/vnd.ibm.minipay mpy -application/vnd.ibm.modcap afp listafp list3820 -application/vnd.ibm.rights-management irm -application/vnd.ibm.secure-container sc -application/vnd.iccprofile icc icm -application/vnd.igloader igl -application/vnd.immervision-ivp ivp -application/vnd.immervision-ivu ivu -# application/vnd.informedcontrol.rms+xml -# application/vnd.informix-visionary -# application/vnd.infotech.project -# application/vnd.infotech.project+xml -application/vnd.insors.igm igm -application/vnd.intercon.formnet xpw xpx -application/vnd.intergeo i2g -# application/vnd.intertrust.digibox -# application/vnd.intertrust.nncp -application/vnd.intu.qbo qbo -application/vnd.intu.qfx qfx -# application/vnd.iptc.g2.conceptitem+xml -# application/vnd.iptc.g2.knowledgeitem+xml -# application/vnd.iptc.g2.newsitem+xml -# application/vnd.iptc.g2.packageitem+xml -application/vnd.ipunplugged.rcprofile rcprofile -application/vnd.irepository.package+xml irp -application/vnd.is-xpr xpr -application/vnd.isac.fcs fcs -application/vnd.jam jam -# application/vnd.japannet-directory-service -# application/vnd.japannet-jpnstore-wakeup -# application/vnd.japannet-payment-wakeup -# application/vnd.japannet-registration -# application/vnd.japannet-registration-wakeup -# application/vnd.japannet-setstore-wakeup -# application/vnd.japannet-verification -# application/vnd.japannet-verification-wakeup -application/vnd.jcp.javame.midlet-rms rms -application/vnd.jisp jisp -application/vnd.joost.joda-archive joda -application/vnd.kahootz ktz ktr -application/vnd.kde.karbon karbon -application/vnd.kde.kchart chrt -application/vnd.kde.kformula kfo -application/vnd.kde.kivio flw -application/vnd.kde.kontour kon -application/vnd.kde.kpresenter kpr kpt -application/vnd.kde.kspread ksp -application/vnd.kde.kword kwd kwt -application/vnd.kenameaapp htke -application/vnd.kidspiration kia -application/vnd.kinar kne knp -application/vnd.koan skp skd skt skm -application/vnd.kodak-descriptor sse -application/vnd.las.las+xml lasxml -# application/vnd.liberty-request+xml -application/vnd.llamagraphics.life-balance.desktop lbd -application/vnd.llamagraphics.life-balance.exchange+xml lbe -application/vnd.lotus-1-2-3 123 -application/vnd.lotus-approach apr -application/vnd.lotus-freelance pre -application/vnd.lotus-notes nsf -application/vnd.lotus-organizer org -application/vnd.lotus-screencam scm -application/vnd.lotus-wordpro lwp -application/vnd.macports.portpkg portpkg -# application/vnd.marlin.drm.actiontoken+xml -# application/vnd.marlin.drm.conftoken+xml -# application/vnd.marlin.drm.license+xml -# application/vnd.marlin.drm.mdcf -application/vnd.mcd mcd -application/vnd.medcalcdata mc1 -application/vnd.mediastation.cdkey cdkey -# application/vnd.meridian-slingshot -application/vnd.mfer mwf -application/vnd.mfmp mfm -application/vnd.micrografx.flo flo -application/vnd.micrografx.igx igx -application/vnd.mif mif -# application/vnd.minisoft-hp3000-save -# application/vnd.mitsubishi.misty-guard.trustweb -application/vnd.mobius.daf daf -application/vnd.mobius.dis dis -application/vnd.mobius.mbk mbk -application/vnd.mobius.mqy mqy -application/vnd.mobius.msl msl -application/vnd.mobius.plc plc -application/vnd.mobius.txf txf -application/vnd.mophun.application mpn -application/vnd.mophun.certificate mpc -# application/vnd.motorola.flexsuite -# application/vnd.motorola.flexsuite.adsi -# application/vnd.motorola.flexsuite.fis -# application/vnd.motorola.flexsuite.gotap -# application/vnd.motorola.flexsuite.kmr -# application/vnd.motorola.flexsuite.ttc -# application/vnd.motorola.flexsuite.wem -# application/vnd.motorola.iprm -application/vnd.mozilla.xul+xml xul -application/vnd.ms-artgalry cil -# application/vnd.ms-asf -application/vnd.ms-cab-compressed cab -application/vnd.ms-excel xls xlm xla xlc xlt xlw -application/vnd.ms-excel.addin.macroenabled.12 xlam -application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb -application/vnd.ms-excel.sheet.macroenabled.12 xlsm -application/vnd.ms-excel.template.macroenabled.12 xltm -application/vnd.ms-fontobject eot -application/vnd.ms-htmlhelp chm -application/vnd.ms-ims ims -application/vnd.ms-lrm lrm -# application/vnd.ms-office.activex+xml -application/vnd.ms-officetheme thmx -application/vnd.ms-pki.seccat cat -application/vnd.ms-pki.stl stl -# application/vnd.ms-playready.initiator+xml -application/vnd.ms-powerpoint ppt pps pot -application/vnd.ms-powerpoint.addin.macroenabled.12 ppam -application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm -application/vnd.ms-powerpoint.slide.macroenabled.12 sldm -application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm -application/vnd.ms-powerpoint.template.macroenabled.12 potm -application/vnd.ms-project mpp mpt -# application/vnd.ms-tnef -# application/vnd.ms-wmdrm.lic-chlg-req -# application/vnd.ms-wmdrm.lic-resp -# application/vnd.ms-wmdrm.meter-chlg-req -# application/vnd.ms-wmdrm.meter-resp -application/vnd.ms-word.document.macroenabled.12 docm -application/vnd.ms-word.template.macroenabled.12 dotm -application/vnd.ms-works wps wks wcm wdb -application/vnd.ms-wpl wpl -application/vnd.ms-xpsdocument xps -application/vnd.mseq mseq -# application/vnd.msign -# application/vnd.multiad.creator -# application/vnd.multiad.creator.cif -# application/vnd.music-niff -application/vnd.musician mus -application/vnd.muvee.style msty -application/vnd.mynfc taglet -# application/vnd.ncd.control -# application/vnd.ncd.reference -# application/vnd.nervana -# application/vnd.netfpx -application/vnd.neurolanguage.nlu nlu -application/vnd.noblenet-directory nnd -application/vnd.noblenet-sealer nns -application/vnd.noblenet-web nnw -# application/vnd.nokia.catalogs -# application/vnd.nokia.conml+wbxml -# application/vnd.nokia.conml+xml -# application/vnd.nokia.isds-radio-presets -# application/vnd.nokia.iptv.config+xml -# application/vnd.nokia.landmark+wbxml -# application/vnd.nokia.landmark+xml -# application/vnd.nokia.landmarkcollection+xml -# application/vnd.nokia.n-gage.ac+xml -application/vnd.nokia.n-gage.data ngdat -application/vnd.nokia.n-gage.symbian.install n-gage -# application/vnd.nokia.ncd -# application/vnd.nokia.pcd+wbxml -# application/vnd.nokia.pcd+xml -application/vnd.nokia.radio-preset rpst -application/vnd.nokia.radio-presets rpss -application/vnd.novadigm.edm edm -application/vnd.novadigm.edx edx -application/vnd.novadigm.ext ext -# application/vnd.ntt-local.file-transfer -# application/vnd.ntt-local.sip-ta_remote -# application/vnd.ntt-local.sip-ta_tcp_stream -application/vnd.oasis.opendocument.chart odc -application/vnd.oasis.opendocument.chart-template otc -application/vnd.oasis.opendocument.database odb -application/vnd.oasis.opendocument.formula odf -application/vnd.oasis.opendocument.formula-template odft -application/vnd.oasis.opendocument.graphics odg -application/vnd.oasis.opendocument.graphics-template otg -application/vnd.oasis.opendocument.image odi -application/vnd.oasis.opendocument.image-template oti -application/vnd.oasis.opendocument.presentation odp -application/vnd.oasis.opendocument.presentation-template otp -application/vnd.oasis.opendocument.spreadsheet ods -application/vnd.oasis.opendocument.spreadsheet-template ots -application/vnd.oasis.opendocument.text odt -application/vnd.oasis.opendocument.text-master odm -application/vnd.oasis.opendocument.text-template ott -application/vnd.oasis.opendocument.text-web oth -# application/vnd.obn -# application/vnd.oftn.l10n+json -# application/vnd.oipf.contentaccessdownload+xml -# application/vnd.oipf.contentaccessstreaming+xml -# application/vnd.oipf.cspg-hexbinary -# application/vnd.oipf.dae.svg+xml -# application/vnd.oipf.dae.xhtml+xml -# application/vnd.oipf.mippvcontrolmessage+xml -# application/vnd.oipf.pae.gem -# application/vnd.oipf.spdiscovery+xml -# application/vnd.oipf.spdlist+xml -# application/vnd.oipf.ueprofile+xml -# application/vnd.oipf.userprofile+xml -application/vnd.olpc-sugar xo -# application/vnd.oma-scws-config -# application/vnd.oma-scws-http-request -# application/vnd.oma-scws-http-response -# application/vnd.oma.bcast.associated-procedure-parameter+xml -# application/vnd.oma.bcast.drm-trigger+xml -# application/vnd.oma.bcast.imd+xml -# application/vnd.oma.bcast.ltkm -# application/vnd.oma.bcast.notification+xml -# application/vnd.oma.bcast.provisioningtrigger -# application/vnd.oma.bcast.sgboot -# application/vnd.oma.bcast.sgdd+xml -# application/vnd.oma.bcast.sgdu -# application/vnd.oma.bcast.simple-symbol-container -# application/vnd.oma.bcast.smartcard-trigger+xml -# application/vnd.oma.bcast.sprov+xml -# application/vnd.oma.bcast.stkm -# application/vnd.oma.cab-address-book+xml -# application/vnd.oma.cab-feature-handler+xml -# application/vnd.oma.cab-pcc+xml -# application/vnd.oma.cab-user-prefs+xml -# application/vnd.oma.dcd -# application/vnd.oma.dcdc -application/vnd.oma.dd2+xml dd2 -# application/vnd.oma.drm.risd+xml -# application/vnd.oma.group-usage-list+xml -# application/vnd.oma.pal+xml -# application/vnd.oma.poc.detailed-progress-report+xml -# application/vnd.oma.poc.final-report+xml -# application/vnd.oma.poc.groups+xml -# application/vnd.oma.poc.invocation-descriptor+xml -# application/vnd.oma.poc.optimized-progress-report+xml -# application/vnd.oma.push -# application/vnd.oma.scidm.messages+xml -# application/vnd.oma.xcap-directory+xml -# application/vnd.omads-email+xml -# application/vnd.omads-file+xml -# application/vnd.omads-folder+xml -# application/vnd.omaloc-supl-init -application/vnd.openofficeorg.extension oxt -# application/vnd.openxmlformats-officedocument.custom-properties+xml -# application/vnd.openxmlformats-officedocument.customxmlproperties+xml -# application/vnd.openxmlformats-officedocument.drawing+xml -# application/vnd.openxmlformats-officedocument.drawingml.chart+xml -# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml -# application/vnd.openxmlformats-officedocument.extended-properties+xml -# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml -# application/vnd.openxmlformats-officedocument.presentationml.comments+xml -# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml -application/vnd.openxmlformats-officedocument.presentationml.presentation pptx -# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml -application/vnd.openxmlformats-officedocument.presentationml.slide sldx -# application/vnd.openxmlformats-officedocument.presentationml.slide+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml -application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx -# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml -# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml -# application/vnd.openxmlformats-officedocument.presentationml.tags+xml -application/vnd.openxmlformats-officedocument.presentationml.template potx -# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx -# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml -# application/vnd.openxmlformats-officedocument.theme+xml -# application/vnd.openxmlformats-officedocument.themeoverride+xml -# application/vnd.openxmlformats-officedocument.vmldrawing -# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.document docx -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx -# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml -# application/vnd.openxmlformats-package.core-properties+xml -# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml -# application/vnd.openxmlformats-package.relationships+xml -# application/vnd.quobject-quoxdocument -# application/vnd.osa.netdeploy -application/vnd.osgeo.mapguide.package mgp -# application/vnd.osgi.bundle -application/vnd.osgi.dp dp -# application/vnd.otps.ct-kip+xml -application/vnd.palm pdb pqa oprc -# application/vnd.paos.xml -application/vnd.pawaafile paw -application/vnd.pg.format str -application/vnd.pg.osasli ei6 -# application/vnd.piaccess.application-licence -application/vnd.picsel efif -application/vnd.pmi.widget wg -# application/vnd.poc.group-advertisement+xml -application/vnd.pocketlearn plf -application/vnd.powerbuilder6 pbd -# application/vnd.powerbuilder6-s -# application/vnd.powerbuilder7 -# application/vnd.powerbuilder7-s -# application/vnd.powerbuilder75 -# application/vnd.powerbuilder75-s -# application/vnd.preminet -application/vnd.previewsystems.box box -application/vnd.proteus.magazine mgz -application/vnd.publishare-delta-tree qps -application/vnd.pvi.ptid1 ptid -# application/vnd.pwg-multiplexed -# application/vnd.pwg-xhtml-print+xml -# application/vnd.qualcomm.brew-app-res -application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb -# application/vnd.radisys.moml+xml -# application/vnd.radisys.msml+xml -# application/vnd.radisys.msml-audit+xml -# application/vnd.radisys.msml-audit-conf+xml -# application/vnd.radisys.msml-audit-conn+xml -# application/vnd.radisys.msml-audit-dialog+xml -# application/vnd.radisys.msml-audit-stream+xml -# application/vnd.radisys.msml-conf+xml -# application/vnd.radisys.msml-dialog+xml -# application/vnd.radisys.msml-dialog-base+xml -# application/vnd.radisys.msml-dialog-fax-detect+xml -# application/vnd.radisys.msml-dialog-fax-sendrecv+xml -# application/vnd.radisys.msml-dialog-group+xml -# application/vnd.radisys.msml-dialog-speech+xml -# application/vnd.radisys.msml-dialog-transform+xml -# application/vnd.rainstor.data -# application/vnd.rapid -application/vnd.realvnc.bed bed -application/vnd.recordare.musicxml mxl -application/vnd.recordare.musicxml+xml musicxml -# application/vnd.renlearn.rlprint -application/vnd.rig.cryptonote cryptonote -application/vnd.rim.cod cod -application/vnd.rn-realmedia rm -application/vnd.route66.link66+xml link66 -# application/vnd.ruckus.download -# application/vnd.s3sms -application/vnd.sailingtracker.track st -# application/vnd.sbm.cid -# application/vnd.sbm.mid2 -# application/vnd.scribus -# application/vnd.sealed.3df -# application/vnd.sealed.csf -# application/vnd.sealed.doc -# application/vnd.sealed.eml -# application/vnd.sealed.mht -# application/vnd.sealed.net -# application/vnd.sealed.ppt -# application/vnd.sealed.tiff -# application/vnd.sealed.xls -# application/vnd.sealedmedia.softseal.html -# application/vnd.sealedmedia.softseal.pdf -application/vnd.seemail see -application/vnd.sema sema -application/vnd.semd semd -application/vnd.semf semf -application/vnd.shana.informed.formdata ifm -application/vnd.shana.informed.formtemplate itp -application/vnd.shana.informed.interchange iif -application/vnd.shana.informed.package ipk -application/vnd.simtech-mindmapper twd twds -application/vnd.smaf mmf -# application/vnd.smart.notebook -application/vnd.smart.teacher teacher -# application/vnd.software602.filler.form+xml -# application/vnd.software602.filler.form-xml-zip -application/vnd.solent.sdkm+xml sdkm sdkd -application/vnd.spotfire.dxp dxp -application/vnd.spotfire.sfs sfs -# application/vnd.sss-cod -# application/vnd.sss-dtf -# application/vnd.sss-ntf -application/vnd.stardivision.calc sdc -application/vnd.stardivision.draw sda -application/vnd.stardivision.impress sdd -application/vnd.stardivision.math smf -application/vnd.stardivision.writer sdw vor -application/vnd.stardivision.writer-global sgl -application/vnd.stepmania.package smzip -application/vnd.stepmania.stepchart sm -# application/vnd.street-stream -application/vnd.sun.xml.calc sxc -application/vnd.sun.xml.calc.template stc -application/vnd.sun.xml.draw sxd -application/vnd.sun.xml.draw.template std -application/vnd.sun.xml.impress sxi -application/vnd.sun.xml.impress.template sti -application/vnd.sun.xml.math sxm -application/vnd.sun.xml.writer sxw -application/vnd.sun.xml.writer.global sxg -application/vnd.sun.xml.writer.template stw -# application/vnd.sun.wadl+xml -application/vnd.sus-calendar sus susp -application/vnd.svd svd -# application/vnd.swiftview-ics -application/vnd.symbian.install sis sisx -application/vnd.syncml+xml xsm -application/vnd.syncml.dm+wbxml bdm -application/vnd.syncml.dm+xml xdm -# application/vnd.syncml.dm.notification -# application/vnd.syncml.ds.notification -application/vnd.tao.intent-module-archive tao -application/vnd.tcpdump.pcap pcap cap dmp -application/vnd.tmobile-livetv tmo -application/vnd.trid.tpt tpt -application/vnd.triscape.mxs mxs -application/vnd.trueapp tra -# application/vnd.truedoc -# application/vnd.ubisoft.webplayer -application/vnd.ufdl ufd ufdl -application/vnd.uiq.theme utz -application/vnd.umajin umj -application/vnd.unity unityweb -application/vnd.uoml+xml uoml -# application/vnd.uplanet.alert -# application/vnd.uplanet.alert-wbxml -# application/vnd.uplanet.bearer-choice -# application/vnd.uplanet.bearer-choice-wbxml -# application/vnd.uplanet.cacheop -# application/vnd.uplanet.cacheop-wbxml -# application/vnd.uplanet.channel -# application/vnd.uplanet.channel-wbxml -# application/vnd.uplanet.list -# application/vnd.uplanet.list-wbxml -# application/vnd.uplanet.listcmd -# application/vnd.uplanet.listcmd-wbxml -# application/vnd.uplanet.signal -application/vnd.vcx vcx -# application/vnd.vd-study -# application/vnd.vectorworks -# application/vnd.verimatrix.vcas -# application/vnd.vidsoft.vidconference -application/vnd.visio vsd vst vss vsw -application/vnd.visionary vis -# application/vnd.vividence.scriptfile -application/vnd.vsf vsf -# application/vnd.wap.sic -# application/vnd.wap.slc -application/vnd.wap.wbxml wbxml -application/vnd.wap.wmlc wmlc -application/vnd.wap.wmlscriptc wmlsc -application/vnd.webturbo wtb -# application/vnd.wfa.wsc -# application/vnd.wmc -# application/vnd.wmf.bootstrap -# application/vnd.wolfram.mathematica -# application/vnd.wolfram.mathematica.package -application/vnd.wolfram.player nbp -application/vnd.wordperfect wpd -application/vnd.wqd wqd -# application/vnd.wrq-hp3000-labelled -application/vnd.wt.stf stf -# application/vnd.wv.csp+wbxml -# application/vnd.wv.csp+xml -# application/vnd.wv.ssp+xml -application/vnd.xara xar -application/vnd.xfdl xfdl -# application/vnd.xfdl.webform -# application/vnd.xmi+xml -# application/vnd.xmpie.cpkg -# application/vnd.xmpie.dpkg -# application/vnd.xmpie.plan -# application/vnd.xmpie.ppkg -# application/vnd.xmpie.xlim -application/vnd.yamaha.hv-dic hvd -application/vnd.yamaha.hv-script hvs -application/vnd.yamaha.hv-voice hvp -application/vnd.yamaha.openscoreformat osf -application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg -# application/vnd.yamaha.remote-setup -application/vnd.yamaha.smaf-audio saf -application/vnd.yamaha.smaf-phrase spf -# application/vnd.yamaha.through-ngn -# application/vnd.yamaha.tunnel-udpencap -application/vnd.yellowriver-custom-menu cmp -application/vnd.zul zir zirz -application/vnd.zzazz.deck+xml zaz -application/voicexml+xml vxml -# application/vq-rtcpxr -# application/watcherinfo+xml -# application/whoispp-query -# application/whoispp-response -application/widget wgt -application/winhlp hlp -# application/wita -# application/wordperfect5.1 -application/wsdl+xml wsdl -application/wspolicy+xml wspolicy -application/x-7z-compressed 7z -application/x-abiword abw -application/x-ace-compressed ace -application/x-authorware-bin aab x32 u32 vox -application/x-authorware-map aam -application/x-authorware-seg aas -application/x-bcpio bcpio -application/x-bittorrent torrent -application/x-bzip bz -application/x-bzip2 bz2 boz -application/x-cdlink vcd -application/x-chat chat -application/x-chess-pgn pgn -# application/x-compress -application/x-cpio cpio -application/x-csh csh -application/x-debian-package deb udeb -application/x-director dir dcr dxr cst cct cxt w3d fgd swa -application/x-doom wad -application/x-dtbncx+xml ncx -application/x-dtbook+xml dtb -application/x-dtbresource+xml res -application/x-dvi dvi -application/x-font-bdf bdf -# application/x-font-dos -# application/x-font-framemaker -application/x-font-ghostscript gsf -# application/x-font-libgrx -application/x-font-linux-psf psf -application/x-font-otf otf -application/x-font-pcf pcf -application/x-font-snf snf -# application/x-font-speedo -# application/x-font-sunos-news -application/x-font-ttf ttf ttc -application/x-font-type1 pfa pfb pfm afm -application/x-font-woff woff -# application/x-font-vfont -application/x-futuresplash spl -application/x-gnumeric gnumeric -application/x-gtar gtar -# application/x-gzip -application/x-hdf hdf -application/x-java-jnlp-file jnlp -application/x-latex latex -application/x-mobipocket-ebook prc mobi -application/x-ms-application application -application/x-ms-wmd wmd -application/x-ms-wmz wmz -application/x-ms-xbap xbap -application/x-msaccess mdb -application/x-msbinder obd -application/x-mscardfile crd -application/x-msclip clp -application/x-msdownload exe dll com bat msi -application/x-msmediaview mvb m13 m14 -application/x-msmetafile wmf -application/x-msmoney mny -application/x-mspublisher pub -application/x-msschedule scd -application/x-msterminal trm -application/x-mswrite wri -application/x-netcdf nc cdf -application/x-pkcs12 p12 pfx -application/x-pkcs7-certificates p7b spc -application/x-pkcs7-certreqresp p7r -application/x-rar-compressed rar -application/x-sh sh -application/x-shar shar -application/x-shockwave-flash swf -application/x-silverlight-app xap -application/x-stuffit sit -application/x-stuffitx sitx -application/x-sv4cpio sv4cpio -application/x-sv4crc sv4crc -application/x-tar tar -application/x-tcl tcl -application/x-tex tex -application/x-tex-tfm tfm -application/x-texinfo texinfo texi -application/x-ustar ustar -application/x-wais-source src -application/x-x509-ca-cert der crt -application/x-xfig fig -application/x-xpinstall xpi -# application/x400-bp -# application/xcap-att+xml -# application/xcap-caps+xml -application/xcap-diff+xml xdf -# application/xcap-el+xml -# application/xcap-error+xml -# application/xcap-ns+xml -# application/xcon-conference-info-diff+xml -# application/xcon-conference-info+xml -application/xenc+xml xenc -application/xhtml+xml xhtml xht -# application/xhtml-voice+xml -application/xml xml xsl -application/xml-dtd dtd -# application/xml-external-parsed-entity -# application/xmpp+xml -application/xop+xml xop -application/xslt+xml xslt -application/xspf+xml xspf -application/xv+xml mxml xhvml xvml xvm -application/yang yang -application/yin+xml yin -application/zip zip -# audio/1d-interleaved-parityfec -# audio/32kadpcm -# audio/3gpp -# audio/3gpp2 -# audio/ac3 -audio/adpcm adp -# audio/amr -# audio/amr-wb -# audio/amr-wb+ -# audio/asc -# audio/atrac-advanced-lossless -# audio/atrac-x -# audio/atrac3 -audio/basic au snd -# audio/bv16 -# audio/bv32 -# audio/clearmode -# audio/cn -# audio/dat12 -# audio/dls -# audio/dsr-es201108 -# audio/dsr-es202050 -# audio/dsr-es202211 -# audio/dsr-es202212 -# audio/dv -# audio/dvi4 -# audio/eac3 -# audio/evrc -# audio/evrc-qcp -# audio/evrc0 -# audio/evrc1 -# audio/evrcb -# audio/evrcb0 -# audio/evrcb1 -# audio/evrcwb -# audio/evrcwb0 -# audio/evrcwb1 -# audio/example -# audio/fwdred -# audio/g719 -# audio/g722 -# audio/g7221 -# audio/g723 -# audio/g726-16 -# audio/g726-24 -# audio/g726-32 -# audio/g726-40 -# audio/g728 -# audio/g729 -# audio/g7291 -# audio/g729d -# audio/g729e -# audio/gsm -# audio/gsm-efr -# audio/gsm-hr-08 -# audio/ilbc -# audio/ip-mr_v2.5 -# audio/l16 -# audio/l20 -# audio/l24 -# audio/l8 -# audio/lpc -audio/midi mid midi kar rmi -# audio/mobile-xmf -audio/mp4 mp4a -# audio/mp4a-latm -# audio/mpa -# audio/mpa-robust -audio/mpeg mpga mp2 mp2a mp3 m2a m3a -# audio/mpeg4-generic -audio/ogg oga ogg spx -# audio/parityfec -# audio/pcma -# audio/pcma-wb -# audio/pcmu-wb -# audio/pcmu -# audio/prs.sid -# audio/qcelp -# audio/red -# audio/rtp-enc-aescm128 -# audio/rtp-midi -# audio/rtx -# audio/smv -# audio/smv0 -# audio/smv-qcp -# audio/sp-midi -# audio/speex -# audio/t140c -# audio/t38 -# audio/telephone-event -# audio/tone -# audio/uemclip -# audio/ulpfec -# audio/vdvi -# audio/vmr-wb -# audio/vnd.3gpp.iufp -# audio/vnd.4sb -# audio/vnd.audiokoz -# audio/vnd.celp -# audio/vnd.cisco.nse -# audio/vnd.cmles.radio-events -# audio/vnd.cns.anp1 -# audio/vnd.cns.inf1 -audio/vnd.dece.audio uva uvva -audio/vnd.digital-winds eol -# audio/vnd.dlna.adts -# audio/vnd.dolby.heaac.1 -# audio/vnd.dolby.heaac.2 -# audio/vnd.dolby.mlp -# audio/vnd.dolby.mps -# audio/vnd.dolby.pl2 -# audio/vnd.dolby.pl2x -# audio/vnd.dolby.pl2z -# audio/vnd.dolby.pulse.1 -audio/vnd.dra dra -audio/vnd.dts dts -audio/vnd.dts.hd dtshd -# audio/vnd.dvb.file dvb -# audio/vnd.everad.plj -# audio/vnd.hns.audio -audio/vnd.lucent.voice lvp -audio/vnd.ms-playready.media.pya pya -# audio/vnd.nokia.mobile-xmf -# audio/vnd.nortel.vbk -audio/vnd.nuera.ecelp4800 ecelp4800 -audio/vnd.nuera.ecelp7470 ecelp7470 -audio/vnd.nuera.ecelp9600 ecelp9600 -# audio/vnd.octel.sbc -# audio/vnd.qcelp -# audio/vnd.rhetorex.32kadpcm -audio/vnd.rip rip -# audio/vnd.sealedmedia.softseal.mpeg -# audio/vnd.vmx.cvsd -# audio/vorbis -# audio/vorbis-config -audio/webm weba -audio/x-aac aac -audio/x-aiff aif aiff aifc -audio/x-mpegurl m3u -audio/x-ms-wax wax -audio/x-ms-wma wma -audio/x-pn-realaudio ram ra -audio/x-pn-realaudio-plugin rmp -audio/x-wav wav -chemical/x-cdx cdx -chemical/x-cif cif -chemical/x-cmdf cmdf -chemical/x-cml cml -chemical/x-csml csml -# chemical/x-pdb -chemical/x-xyz xyz -image/bmp bmp -image/cgm cgm -# image/example -# image/fits -image/g3fax g3 -image/gif gif -image/ief ief -# image/jp2 -image/jpeg jpeg jpg jpe -# image/jpm -# image/jpx -image/ktx ktx -# image/naplps -image/png png -image/prs.btif btif -# image/prs.pti -image/svg+xml svg svgz -# image/t38 -image/tiff tiff tif -# image/tiff-fx -image/vnd.adobe.photoshop psd -# image/vnd.cns.inf2 -image/vnd.dece.graphic uvi uvvi uvg uvvg -image/vnd.dvb.subtitle sub -image/vnd.djvu djvu djv -image/vnd.dwg dwg -image/vnd.dxf dxf -image/vnd.fastbidsheet fbs -image/vnd.fpx fpx -image/vnd.fst fst -image/vnd.fujixerox.edmics-mmr mmr -image/vnd.fujixerox.edmics-rlc rlc -# image/vnd.globalgraphics.pgb -# image/vnd.microsoft.icon -# image/vnd.mix -image/vnd.ms-modi mdi -image/vnd.net-fpx npx -# image/vnd.radiance -# image/vnd.sealed.png -# image/vnd.sealedmedia.softseal.gif -# image/vnd.sealedmedia.softseal.jpg -# image/vnd.svf -image/vnd.wap.wbmp wbmp -image/vnd.xiff xif -image/webp webp -image/x-cmu-raster ras -image/x-cmx cmx -image/x-freehand fh fhc fh4 fh5 fh7 -image/x-icon ico -image/x-pcx pcx -image/x-pict pic pct -image/x-portable-anymap pnm -image/x-portable-bitmap pbm -image/x-portable-graymap pgm -image/x-portable-pixmap ppm -image/x-rgb rgb -image/x-xbitmap xbm -image/x-xpixmap xpm -image/x-xwindowdump xwd -# message/cpim -# message/delivery-status -# message/disposition-notification -# message/example -# message/external-body -# message/feedback-report -# message/global -# message/global-delivery-status -# message/global-disposition-notification -# message/global-headers -# message/http -# message/imdn+xml -# message/news -# message/partial -message/rfc822 eml mime -# message/s-http -# message/sip -# message/sipfrag -# message/tracking-status -# message/vnd.si.simp -# model/example -model/iges igs iges -model/mesh msh mesh silo -model/vnd.collada+xml dae -model/vnd.dwf dwf -# model/vnd.flatland.3dml -model/vnd.gdl gdl -# model/vnd.gs-gdl -# model/vnd.gs.gdl -model/vnd.gtw gtw -# model/vnd.moml+xml -model/vnd.mts mts -# model/vnd.parasolid.transmit.binary -# model/vnd.parasolid.transmit.text -model/vnd.vtu vtu -model/vrml wrl vrml -# multipart/alternative -# multipart/appledouble -# multipart/byteranges -# multipart/digest -# multipart/encrypted -# multipart/example -# multipart/form-data -# multipart/header-set -# multipart/mixed -# multipart/parallel -# multipart/related -# multipart/report -# multipart/signed -# multipart/voice-message -# text/1d-interleaved-parityfec -text/calendar ics ifb -text/css css -text/csv csv -# text/directory -# text/dns -# text/ecmascript -# text/enriched -# text/example -# text/fwdred -text/html html htm -# text/javascript -text/n3 n3 -# text/parityfec -text/plain txt text conf def list log in -# text/prs.fallenstein.rst -text/prs.lines.tag dsc -# text/vnd.radisys.msml-basic-layout -# text/red -# text/rfc822-headers -text/richtext rtx -# text/rtf -# text/rtp-enc-aescm128 -# text/rtx -text/sgml sgml sgm -# text/t140 -text/tab-separated-values tsv -text/troff t tr roff man me ms -text/turtle ttl -# text/ulpfec -text/uri-list uri uris urls -text/vcard vcard -# text/vnd.abc -text/vnd.curl curl -text/vnd.curl.dcurl dcurl -text/vnd.curl.scurl scurl -text/vnd.curl.mcurl mcurl -# text/vnd.dmclientscript -text/vnd.dvb.subtitle sub -# text/vnd.esmertec.theme-descriptor -text/vnd.fly fly -text/vnd.fmi.flexstor flx -text/vnd.graphviz gv -text/vnd.in3d.3dml 3dml -text/vnd.in3d.spot spot -# text/vnd.iptc.newsml -# text/vnd.iptc.nitf -# text/vnd.latex-z -# text/vnd.motorola.reflex -# text/vnd.ms-mediapackage -# text/vnd.net2phone.commcenter.command -# text/vnd.si.uricatalogue -text/vnd.sun.j2me.app-descriptor jad -# text/vnd.trolltech.linguist -# text/vnd.wap.si -# text/vnd.wap.sl -text/vnd.wap.wml wml -text/vnd.wap.wmlscript wmls -text/x-asm s asm -text/x-c c cc cxx cpp h hh dic -text/x-fortran f for f77 f90 -text/x-pascal p pas -text/x-java-source java -text/x-setext etx -text/x-uuencode uu -text/x-vcalendar vcs -text/x-vcard vcf -# text/xml -# text/xml-external-parsed-entity -# video/1d-interleaved-parityfec -video/3gpp 3gp -# video/3gpp-tt -video/3gpp2 3g2 -# video/bmpeg -# video/bt656 -# video/celb -# video/dv -# video/example -video/h261 h261 -video/h263 h263 -# video/h263-1998 -# video/h263-2000 -video/h264 h264 -# video/h264-rcdo -# video/h264-svc -video/jpeg jpgv -# video/jpeg2000 -video/jpm jpm jpgm -video/mj2 mj2 mjp2 -# video/mp1s -# video/mp2p -# video/mp2t -video/mp4 mp4 mp4v mpg4 -# video/mp4v-es -video/mpeg mpeg mpg mpe m1v m2v -# video/mpeg4-generic -# video/mpv -# video/nv -video/ogg ogv -# video/parityfec -# video/pointer -video/quicktime qt mov -# video/raw -# video/rtp-enc-aescm128 -# video/rtx -# video/smpte292m -# video/ulpfec -# video/vc1 -# video/vnd.cctv -video/vnd.dece.hd uvh uvvh -video/vnd.dece.mobile uvm uvvm -# video/vnd.dece.mp4 -video/vnd.dece.pd uvp uvvp -video/vnd.dece.sd uvs uvvs -video/vnd.dece.video uvv uvvv -# video/vnd.directv.mpeg -# video/vnd.directv.mpeg-tts -# video/vnd.dlna.mpeg-tts -video/vnd.dvb.file dvb -video/vnd.fvt fvt -# video/vnd.hns.video -# video/vnd.iptvforum.1dparityfec-1010 -# video/vnd.iptvforum.1dparityfec-2005 -# video/vnd.iptvforum.2dparityfec-1010 -# video/vnd.iptvforum.2dparityfec-2005 -# video/vnd.iptvforum.ttsavc -# video/vnd.iptvforum.ttsmpeg2 -# video/vnd.motorola.video -# video/vnd.motorola.videop -video/vnd.mpegurl mxu m4u -video/vnd.ms-playready.media.pyv pyv -# video/vnd.nokia.interleaved-multimedia -# video/vnd.nokia.videovoip -# video/vnd.objectvideo -# video/vnd.sealed.mpeg1 -# video/vnd.sealed.mpeg4 -# video/vnd.sealed.swf -# video/vnd.sealedmedia.softseal.mov -video/vnd.uvvu.mp4 uvu uvvu -video/vnd.vivo viv -video/webm webm -video/x-f4v f4v -video/x-fli fli -video/x-flv flv -video/x-m4v m4v -video/x-ms-asf asf asx -video/x-ms-wm wm -video/x-ms-wmv wmv -video/x-ms-wmx wmx -video/x-ms-wvx wvx -video/x-msvideo avi -video/x-sgi-movie movie -x-conference/x-cooltalk ice +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpp-ims+xml +# application/activemessage +application/andrew-inset ez +# application/applefile +application/applixware aw +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomicmail +application/atomsvc+xml atomsvc +# application/auth-policy+xml +# application/batch-smtp +# application/beep+xml +# application/calendar+xml +# application/cals-1840 +# application/ccmp+xml +application/ccxml+xml ccxml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cnrp+xml +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csta+xml +# application/cstadata+xml +application/cu-seeme cu +# application/cybercash +application/davmount+xml davmount +# application/dca-rft +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dns +application/docbook+xml dbk +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +application/emma+xml emma +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fits +application/font-tdpfr pfr +# application/framework-attributes+xml +application/gml+xml gml +application/gpx+xml gpx +application/gxf gxf +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-request+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +application/json json +application/jsonml+json jsonml +# application/kpml-request+xml +# application/kpml-response+xml +application/lost+xml lostxml +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +# application/mathml-content+xml +# application/mathml-presentation+xml +application/mathml+xml mathml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media_control+xml +application/mediaservercontrol+xml mscml +application/metalink+xml metalink +application/metalink4+xml meta4 +application/mets+xml mets +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-request +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nss +# application/ocsp-request +# application/ocsp-response +application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy +application/oda oda +application/oebps-package+xml opf +application/ogg ogx +application/omdoc+xml omdoc +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssdl+xml ssdl +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/tve-trigger +# application/ulpfec +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp.bsf+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +application/vnd.adobe.formscentral.fcdt fcdt +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +application/vnd.chemdraw+xml cdxml +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.collection+json +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cybank +application/vnd.dart dart +application/vnd.data-vision.rdz rdz +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.dir-bi.plate-dl-nosuffix +application/vnd.dna dna +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +application/vnd.ds-keypoint kpxx +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-request+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.fints +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +# application/vnd.fujixerox.hbpl +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +# application/vnd.hzn-3d-crossword +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +# application/vnd.innopath.wamp.notification +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.newsmessage+xml +# application/vnd.iptc.g2.packageitem+xml +# application/vnd.iptc.g2.planningitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-request+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +# application/vnd.ms-color.iccprofile +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +# application/vnd.ms-opentype +# application/vnd.ms-package.obfuscated-opentype +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +# application/vnd.ms-printing.printticket+xml +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +application/vnd.nitf ntf nitf +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-request +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.quobject-quoxdocument +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +application/vnd.osgi.subsystem esa +# application/vnd.otps.ct-kip+xml +application/vnd.palm pdb pqa oprc +# application/vnd.paos.xml +application/vnd.pawaafile paw +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.rn-realmedia-vbr rmvb +application/vnd.route66.link66+xml link66 +# application/vnd.rs-274x +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +# application/vnd.sun.wadl+xml +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.wsc +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +# application/x-amf +application/x-apple-diskimage dmg +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-blorb blb blorb +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cbr cbr cba cbt cbz cb7 +application/x-cdlink vcd +application/x-cfs-compressed cfs +application/x-chat chat +application/x-chess-pgn pgn +application/x-conference nsc +# application/x-compress +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-dgc-compressed dgc +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-envoy evy +application/x-eva eva +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-otf otf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-ttf ttf ttc +application/x-font-type1 pfa pfb pfm afm +application/x-font-woff woff +# application/x-font-vfont +application/x-freearc arc +application/x-futuresplash spl +application/x-gca-compressed gca +application/x-glulx ulx +application/x-gnumeric gnumeric +application/x-gramps-xml gramps +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-install-instructions install +application/x-iso9660-image iso +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-lzh-compressed lzh lha +application/x-mie mie +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-shortcut lnk +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf wmz emf emz +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-nzb nzb +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-research-info-systems ris +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-sql sql +application/x-stuffit sit +application/x-stuffitx sitx +application/x-subrip srt +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-t3vm-image t3 +application/x-tads gam +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-tgif obj +application/x-ustar ustar +application/x-wais-source src +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xliff+xml xlf +application/x-xpinstall xpi +application/x-xz xz +application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8 +# application/x400-bp +application/xaml+xml xaml +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info-diff+xml +# application/xcon-conference-info+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xmpp+xml +application/xop+xml xop +application/xproc+xml xpl +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/example +# audio/fwdred +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/isac +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +# audio/musepack +audio/ogg oga ogg spx +# audio/opus +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu-wb +# audio/pcmu +# audio/prs.sid +# audio/qcelp +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtx +audio/s3m s3m +audio/silk sil +# audio/smv +# audio/smv0 +# audio/smv-qcp +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-caf caf +audio/x-flac flac +audio/x-matroska mka +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +# audio/x-tta +audio/x-wav wav +audio/xm xm +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +image/sgi sgi +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.dvb.subtitle sub +image/vnd.djvu djvu djv +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +image/vnd.ms-modi mdi +image/vnd.ms-photo wdp +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +image/webp webp +image/x-3ds 3ds +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-mrsid-image sid +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-tga tga +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# model/example +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +model/vnd.vtu vtu +model/vrml wrl vrml +model/x3d+binary x3db x3dbz +model/x3d+vrml x3dv x3dvz +model/x3d+xml x3d x3dz +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# text/1d-interleaved-parityfec +text/cache-manifest appcache +text/calendar ics ifb +text/css css +text/csv csv +# text/directory +# text/dns +# text/ecmascript +# text/enriched +# text/example +# text/fwdred +text/html html htm +# text/javascript +text/n3 n3 +# text/parityfec +text/plain txt text conf def list log in +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/vnd.radisys.msml-basic-layout +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.scurl scurl +text/vnd.curl.mcurl mcurl +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-java-source java +text/x-opml opml +text/x-pascal p pas +text/x-nfo nfo +text/x-setext etx +text/x-sfv sfv +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raw +# video/rtp-enc-aescm128 +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-matroska mkv mk3d mks +video/x-mng mng +video/x-ms-asf asf asx +video/x-ms-vob vob +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +video/x-smv smv +x-conference/x-cooltalk ice diff --git a/websvr/lib/types/node.types b/websvr/lib/types/node.types index 69a6641..988d848 100644 --- a/websvr/lib/types/node.types +++ b/websvr/lib/types/node.types @@ -1,54 +1,71 @@ -# What: Google Chrome Extension -# Why: To allow apps to (work) be served with the right content type header. -# http://codereview.chromium.org/2830017 -# Added by: niftylettuce -application/x-chrome-extension crx - -# What: OTF Message Silencer -# Why: To silence the "Resource interpreted as font but transferred with MIME -# type font/otf" message that occurs in Google Chrome -# Added by: niftylettuce -font/opentype otf - -# What: HTC support -# Why: To properly render .htc files such as CSS3PIE -# Added by: niftylettuce -text/x-component htc - -# What: HTML5 application cache manifest -# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps -# per https://developer.mozilla.org/en/offline_resources_in_firefox -# Added by: louisremi -text/cache-manifest appcache manifest - -# What: node binary buffer format -# Why: semi-standard extension w/in the node community -# Added by: tootallnate -application/octet-stream buffer - -# What: The "protected" MP-4 formats used by iTunes. -# Why: Required for streaming music to browsers (?) -# Added by: broofa -application/mp4 m4p -audio/mp4 m4a - -# What: Music playlist format (http://en.wikipedia.org/wiki/M3U) -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -application/x-mpegURL m3u8 - -# What: Video format, Part of RFC1890 -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -video/MP2T ts - -# What: The FLAC lossless codec format -# Why: Streaming and serving FLAC audio -# Added by: jacobrask -audio/flac flac - -# What: EventSource mime type -# Why: mime type of Server-Sent Events stream -# http://www.w3.org/TR/eventsource/#text-event-stream -# Added by: francois2metz -text/event-stream event-stream +# What: WebVTT +# Why: To allow formats intended for marking up external text track resources. +# http://dev.w3.org/html5/webvtt/ +# Added by: niftylettuce +text/vtt vtt + +# What: Google Chrome Extension +# Why: To allow apps to (work) be served with the right content type header. +# http://codereview.chromium.org/2830017 +# Added by: niftylettuce +application/x-chrome-extension crx + +# What: HTC support +# Why: To properly render .htc files such as CSS3PIE +# Added by: niftylettuce +text/x-component htc + +# What: HTML5 application cache manifest +# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps +# per https://developer.mozilla.org/en/offline_resources_in_firefox +# Added by: louisremi +text/cache-manifest appcache manifest + +# What: node binary buffer format +# Why: semi-standard extension w/in the node community +# Added by: tootallnate +application/octet-stream buffer + +# What: The "protected" MP-4 formats used by iTunes. +# Why: Required for streaming music to browsers (?) +# Added by: broofa +application/mp4 m4p +audio/mp4 m4a + +# What: Video format, Part of RFC1890 +# Why: See https://github.com/bentomas/node-mime/pull/6 +# Added by: mjrusso +video/MP2T ts + +# What: EventSource mime type +# Why: mime type of Server-Sent Events stream +# http://www.w3.org/TR/eventsource/#text-event-stream +# Added by: francois2metz +text/event-stream event-stream + +# What: Mozilla App manifest mime type +# Why: https://developer.mozilla.org/en/Apps/Manifest#Serving_manifests +# Added by: ednapiranha +application/x-web-app-manifest+json webapp + +# What: Lua file types +# Why: Googling around shows de-facto consensus on these +# Added by: creationix (Issue #45) +text/x-lua lua +application/x-lua-bytecode luac + +# What: Markdown files, as per http://daringfireball.net/projects/markdown/syntax +# Why: http://stackoverflow.com/questions/10701983/what-is-the-mime-type-for-markdown +# Added by: avoidwork +text/x-markdown markdown md mkd + + +# What: ini files +# Why: because they're just text files +# Added by: Matthew Kastor +text/plain ini + +# What: DASH Adaptive Streaming manifest +# Why: https://developer.mozilla.org/en-US/docs/DASH_Adaptive_Streaming_for_HTML_5_Video +# Added by: eelcocramer +application/dash+xml mdp diff --git a/websvr/lib/util.js b/websvr/lib/util.js deleted file mode 100644 index 61f6b4c..0000000 --- a/websvr/lib/util.js +++ /dev/null @@ -1,6 +0,0 @@ -// Backwards compatibility ... -try { - module.exports = require('util'); -} catch (e) { - module.exports = require('sys'); -} diff --git a/websvr/tool/Combine.js b/websvr/tool/Combine.js deleted file mode 100644 index fba9156..0000000 --- a/websvr/tool/Combine.js +++ /dev/null @@ -1,226 +0,0 @@ -/* -* Description: Combine the files into one, support directory and config files. -* Author: Kris Zhang -* Blog: http://c52u.com -*/ - -//Combine namespace -var Combine; - -(function() { - - var fs = require("fs"), - path = require("path"), - timerID = null; - - var combine = Combine = module.exports = { - sourceFile: "", - targetFile: "", - files: [], //combine list - list: [], //watch list - watch: false, //listen on the changes? - - //Interface - init: function(sourceFile, targetFile, watch) { - //Combine type, it's a directory or cfg file - fs.stat(sourceFile, function(err, stat) { - if (err) { - console.log(err); - return; - } - - combine.sourceFile = sourceFile; - combine.targetFile = targetFile; - combine.watch = watch; - - //It's a configuration files or dictionary - if (stat.isFile()) { - combine.setCfg(sourceFile); - } else { - combine.setDir(sourceFile); - } - }); - }, - - //get output stream - getStream: function() { - var stream; - try { - stream = fs.createWriteStream(combine.targetFile); - } catch (err) { - console.log("Can't create output stream: ", err); - } - return stream; - }, - - //get files from cfgFile, return absolute file path - getFiles: function(cfgPath) { - var contents = fs.readFileSync(cfgPath, 'utf-8'), - files = [], - lastIdx = cfgPath.lastIndexOf('\\'), - dir = cfgPath.substring(0, lastIdx > -1 ? lastIdx : cfgPath.lastIndexOf('/') ); - - //read a file line-by-line - contents.match(/[^\r\n]+/g).forEach(function(line) { - //ignore comments that begin with '#' - if (line[0] != '#') { - files.push(path.join(dir, line)); - } - }); - - combine.watch && files.forEach(function(file) { - if (combine.list.indexOf(file) < 0) { - combine.watchFile(file); - combine.list.push(file); - }; - }); - - combine.files = files; - - return files; - }, - - //Watch changes on source folder - setDir: function(directory) { - //Combine at the first running, then watching the changes. - if (combine.combineDir(directory)) { - combine.watch && fs.watch(directory, function() { - combine.combineDir(directory); - }); - } - }, - - //Watch chagnes on configuration fiel - setCfg: function(configuration) { - var combineCfg = function() { - //get file list from the configuration files. - combine.getFiles(configuration); - combine.combine(); - }; - - //Listen on the change on the configuration file - combine.watch && fs.watch(configuration, combineCfg); - - //combine at the first running - combineCfg(); - }, - - //Watch changes on a file - watchFile: function(file) { - try { - fs.watch(file, function() { - combine.combine(); - }); - } catch (err) { - console.log(file, err); - } - }, - - //Combine directory - combineDir: function(directory) { - try { - var allFiles = fs.readdirSync(directory), - //File name must be consist of numbers characters or "-" "_", "." - fileReg = /^[a-zA-Z0-9-_\.]+$/, - files = []; - - allFiles.forEach(function(file) { - if (fileReg.test(file)) { - files.push(path.join(directory, file)); - } else { - console.log("Skip file:" + file); - } - }); - - combine.files = files; - - return combine.combine(); - } catch (err) { - console.log(err); - return false; - } - }, - - //Combine set of files into one - combine: function() { - - //Prevent other request within 1 seconds; - if (timerID) return; - - timerID = setTimeout(function() { - timerID = null; - }, 1000); - - var oStream = combine.getStream(), r = true; - if (!oStream) { - return false; - } - - var files = combine.files; - - try { - files.forEach(function(file) { - var stat = fs.statSync(file); - - if (!stat.isFile()) { - console.log("Skip folder:" + file); - } else { - var data = fs.readFileSync(file); - oStream.write("/*" + file + "*/\r\n"); - oStream.write(data); - oStream.write("\r\n"); - - console.log("Adding file:" + file); - } - }); - oStream.end(); - - var endTime = new Date(); - console.log("count:", - files.length, - ", date:", new Date().toTimeString(), - "\r\n\r\n" - ); - } catch (err) { - console.log(err); - r = false; - } - - return r; - } - }; - -})(); - - -/* -* call it from command lines -* -i filepath: input directory or cfg file -* -o filepath: output files -* -w: keep watch the changes? -*/ -(function() { - - /* - * parsing parameters from command line - * etc, node combine.js -i configfile.path -o outputfile.path - * the parameter will be: '-' + one character, like: parsing('-o'); - */ - var parsing = function(args, key) { - if (!key || key.length != 2 || key[0] != '-') return; - - var reg = new RegExp(" " + key + "((?! -\\w).)*", "g"), - param = args.match(reg); - - if (param && param[0]) { - return param[0].substr(4, 500); - } - }; - - var args = process.argv.join(' '), - input = parsing(args, '-i'), - output = parsing(args, '-o'); - - Combine.init(input, output, args.indexOf(' -w') > 0); - -})(); diff --git a/websvr/websvr.js b/websvr/websvr.js index 05d0f7a..bf52590 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1,1133 +1,1142 @@ -/*Global.js*/ -/* -* Description: node-websvr -* Author: Kris Zhang -* Licenses: MIT, GPL -* Project url: https://github.com/newghost/node-websvr -* -* Referenced projects: -* Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -* Underscore: Add underscore a utility-belt library for JavaScript - https://github.com/documentcloud/underscore -* MIME: content-type in header - https://github.com/broofa/node-mime -* template: Template Engine - https://github.com/olado/doT -*/ - -//Node libraries -var fs = require("fs"); -var path = require("path"); -var qs = require("querystring"); - -var http = require("http"); -var https = require("https"); - -//Open source libraries -var mime = require("./lib/mime"); -/*Utility.js*/ -/* -* Utility -*/ -var _ = { - //extend object to target - extend: function(tar, obj) { - if (!obj) return; - - for (var key in obj) { - tar[key] = obj[key]; - } - - return tar; - } -}; -/*Settings.js*/ -/* -Configurations -*/ -var Settings = { - //root folder of web - root: "../", - - //list files in directory - listDir: false, - - //http - http: true, - //default port of http - port: 8054, - - //enable client-side cache(304)? - cache: true, - //enable debug information output - debug: true, - //receive buffer, default size 32k, i.e.: receive post data from ajax request - bufferSize: 32768, - - //default pages, only one is supported - defaultPage: "index.html", - - //https - https: false, - //default port of https - httpsPort: 8443, - httpsOpts: { key:"", cert:"" }, - - //logger file path - logger: "./tmp/log.txt", - - //session file stored here - sessionDir: "./tmp/session", - /* - Session timeout, in milliseconds. - When session is expired, session file will not deleted. - */ - sessionTimeout: 1440000, - /* - Session garbage collection time, in milliseconds. - When session expired time is more than (sessionAge + sessionGCT), - then session file will be deleted. - */ - sessionGarbage: 3460000, - - //tempary upload file stored here - uploadDir: "./tmp/upload" -}; -/*Logger.js*/ -/* -Logger: log sth -*/ -var Logger = (function() { - - var lineSeparator = "\r\n", - indentSeparator = "\t", - depth = 9; - - var Settings = Settings || { debug: true }; - - var write = function(logObj, dep) { - var depth = dep || depth, - output = new Date() + lineSeparator; - - function print(pre, obj) { - if (!obj) return; - for (var key in obj) { - var val = obj[key]; - output = output + pre + key + " : " + val + lineSeparator; - if (typeof val == "object") { - (pre.length < depth) && print(pre + indentSeparator, val); - } - } - } - - print(indentSeparator, logObj); - - fs.appendFile(Settings.logger, output, function(err) { - log(err); - }); - }; - - /* - Currnetly it's equal to console.log - */ - var log = function() { - console.log.apply(console, arguments); - }; - - /* - Add data before log information - */ - var debug = function() { - //diable console.log information - if (!Settings.debug) { - return; - } - - var d = new Date(); - - Array.prototype.splice.call(arguments, 0, 0, d.toISOString()); - console.log.apply(console, arguments); - }; - - return { - log: log - , write: write - , debug: debug - }; - -})(); -/*Mapper.js*/ -/* -Mapper: Used for Filter & Handler, -expression: required parameter -handler: required parameter -options: optional parameters -*/ - -var Mapper = function(expression, handler, options) { - var self = this; - - self.expression = expression; - self.handler = handler; - - //Has other parameters? - self.extend(options); -}; - -Mapper.prototype = { - /* - Does this mapper matched this request? - */ - match: function(req) { - var self = this, - expression = self.expression; - - //No expression? It's a general filter mapper - if (!expression) return true; - - switch (expression.constructor) { - case String: return req.url.indexOf(expression) > -1; - case RegExp: return expression.test(req.url); - } - - return false; - }, - - /* - Add optional parameters on current mapper - i.e: - session: boolean - file: boolean - parse: boolean - */ - extend: function(options) { - for(key in options) { - this[key] = options[key] - } - } -}; -/*BodyParser.js*/ -/* -Body parser, parse the data in request body via -when parse complete, execute the callback, with response data; -*/ -var BodyParser = function(req, res, callback) { - - var buffer = new Buffer(Settings.bufferSize); - - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); -}; -/*SessionManager.js*/ -/* -SessionManager: -- Clear expired session files -- Valid session -*/ -var SessionManager = (function() { - - //duration time - var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; - - //timer - var timer; - - //session array object, stored with {sid: [update time]}; - var list = {}; - - var getPath = function(sid) { - return path.join(Settings.sessionDir, sid); - }; - - //create a new session id - var create = function() { - //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions - var uuid - = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars - + '-' - + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars - + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars - - //fix the length to 25 - uuid += '00000000000000000000'.substr(0, 25 - uuid.length); - - return uuid; - }; - - //force update session in list - var update = function(sid, datetime) { - list[sid] = datetime || new Date(); - }; - - //remove a sesson from list - var remove = function(sid) { - //delete the file - fs.unlink(getPath(sid)); - //remove from list - delete list[sid]; - - Logger.debug("session removed", sid); - }; - - /* - Does session expired? - If the session is not in the list, add to the list. - i.e. When WebSvr restarted, session will not expired. - */ - var isValid = function(sid) { - var now = new Date(); - - !list[sid] && (list[sid] = now); - - return now - list[sid] <= Settings.sessionTimeout - }; - - /* - Session clean handler - */ - var cleanHandler = function() { - for (var sid in list) { - !isValid(sid) && remove(sid); - } - }; - - /* - Clean the session in temp folder - */ - var clean = function() { - fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.debug(err); - - //converted to minutes - var expire = (+new Date() - gcTime) / 60000 | 0; - - files.forEach(function(file) { - if (file.length == 25) { - var stamp = parseInt(file.substr(0, file.indexOf('-'))); - - if (stamp) { - //remove the expired session - stamp < expire - ? remove(file) - : Logger.debug("session skipped", file); - } - } - }); - }); - }; - - //refresh session in list, valid first, if not expired, update the time - var refresh = function(sid, datetime) { - isValid(sid) && update(sid, datetime); - }; - - var stop = function() { - clearInterval(timer); - timer = null; - }; - - //stop before new session start - var start = function() { - //stop cleanHandler if available - stop(); - //clean the old sessions - clean(); - timer = setInterval(cleanHandler, gcTime); - }; - - return { - list: list, - create: create, - update: update, - remove: remove, - refresh: refresh, - isValid: isValid, - getPath: getPath, - clean: clean, - start: start, - stop: stop - } - -})(); -/*SessionParser.js*/ -/* -Parse request with session support -*/ -var SessionParser = function(req, res) { - var self = this; - - //session id - self.sid = null; - //session stored object - self.obj = null; - //is this new session? - self.new = false; - - //init session object - self.init(req, res); -}; - -SessionParser.prototype = { - init: function(req, res) { - var self = this, - sidKey = "_wsid", - sidVal; - - //Get or Create sid, sid exist in the cookie, read it - var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); - - //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 25) { - sidVal = SessionManager.create(); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - self.new = true; - }; - self.sid = sidVal; - - SessionManager.refresh(self.sid); - } - - //Create new session object - , newObj: function(key, cb) { - //Key is offered, return null of this key, else return empty session object - var self = this, - val = key ? null : {}; - - self.obj = {}; - cb && cb(val); - return val; - } - - //Get value from session object - , getVal: function(key, cb) { - var self = this; - - //key is null, return all the session object - var val = key ? self.obj[key] : self.obj; - cb && cb(val); - - return val; - } - - //Set an key/value pair in session object - , set: function(key, val, cb) { - var self = this; - - //Get session object first - self.get(function() { - - //Add or update key/value in session object - self.obj[key] = val; - - //Write or modify json file - fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { - if (err) { - Logger.debug(err); - return; - } - - cb && cb(self.obj); - - //force update - SessionManager.update(self.sid); - }); - }); - } - - //Get value from session file - , get: function(key, cb) { - var self = this; - - //The first parameter is callback function - if (key.constructor == Function) { - cb = key; - key = null; - } - - //The session object is already loaded - if (self.obj) return self.getVal(key, cb); - - //It's a new session file, need not to load it from file - if (self.new) return self.newObj(key, cb); - - var sessionPath = SessionManager.getPath(self.sid); - - //File operates, will cause delay - fs.exists(sessionPath, function(exists) { - //err: file doesn't exist - if (!exists) { - return self.newObj(key, cb); - - //session not expired - } else if (SessionManager.isValid(self.sid)) { - fs.readFile(sessionPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - return self.getVal(key, cb); - }); - - //session expired, treat it as new session - } else { - return self.newObj(key, cb); - } - }); - } - -}; -/*Parser.js*/ -/* -Parser: Functions that Filter and Handler will be called -*/ -var Parser = function(req, res, mapper) { - - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - req.session = new SessionParser(req, res); - } - handler(req, res); - }; - - /* - parse data in request, this should be done before parse session, - because session stored in file - */ - var parseBody = function() { - //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { - //Must parser the request first, or the post data will lost; - BodyParser(req, res, function(data) { - req.body = data; - parseSession(); - }); - }else{ - parseSession(); - } - }; - - /* - parse file in request, this should be at the top of the list - */ - var parseFile = function() { - //Need to parse the file in request? - if (mapper.file && typeof req.body == "undefined") { - //Must parser the request first, or the post data maybe lost; - var formidable = require('./lib/incoming_form'), - form = new formidable.IncomingForm(); - - form.uploadDir = Settings.uploadDir; - - form.parse(req, function(err, fields, files) { - if (err) { - Logger.debug(err); - return; - }; - - //attach the parameters and files - req.body = fields; - req.files = files; - - //in fact request will not be parsed again, because body is not undefined - parseBody(); - }); - }else{ - parseBody(); - }; - }; - - parseFile(); - -}; - -/*Filter.js*/ -/* -Http Filter: Execute all the rules that matched, -Filters will be always called before a handler. -*/ -var Filter = { - //filter list - filters: [], - - /* - filter: add a new filter - expression: string/regexp [optional] - handler: function [required] - options: object [optional] - */ - filter: function(expression, handler, options) { - //The first parameter is Function => (handler, options) - if (expression.constructor == Function) { - options = handler; - handler = expression; - expression = null; - } - - var mapper = new Mapper(expression, handler, options); - Filter.filters.push(mapper); - }, - - /* - file receiver: it's a specfic filter, - this filter should be always at the top of the filter list - */ - file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); - //insert at the top of the filter array - Filter.filters.splice(0, 0, mapper); - } -}; - -/* -Filter Chain -*/ -var FilterChain = function(cb, req, res) { - var self = this; - - self.idx = 0; - self.cb = cb; - - self.req = req; - self.res = res; -}; - -FilterChain.prototype = { - next: function() { - var self = this, - req = self.req, - res = self.res; - - var mapper = Filter.filters[self.idx++]; - - //filter is complete, execute callback; - if (!mapper) return self.cb && self.cb(); - - /* - If not Matched go to next filter - If matched need to execute the req.next() in callback handler, - e.g: - webSvr.filter(/expression/, function(req, res) { - //filter actions - req.next(req, res); - }, options); - */ - if (mapper.match(req)) { - Logger.debug("filter matched", self.idx, mapper.expression, req.url); - - //filter matched, parse the request and then execute it - Parser(req, res, mapper); - }else{ - //filter not matched, validate next filter - self.next(); - } - } -}; -/*Handler.js*/ -/* -Http Handler: Execute and returned when when first matched; -At the same time only one Handler will be called; -*/ -var Handler; - -(function() { - - /* - Private: handler list - */ - var handlers = []; - - /* - Static Handler instance - */ - Handler = { - /* - url: add a new handler - expression: string/regexp [required] - handler: [many types] [required] - options: object [optional] - */ - url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - handlers.push(mapper); - }, - - //Post: Parse the post data by default; - post: function(expression, handler, options) { - this.url(expression, handler, _.extend({ parse: true }, options)); - }, - - //Session: Parse the session and post by default; - session: function(expression, handler) { - this.url(expression, handler, { parse: true, session: true }); - }, - - handle: function(req, res) { - //flag: is matched? - for(var i = 0, len = handlers.length; i < len ; i++) { - - var mapper = handlers[i]; - if (mapper.match(req)) { - - Logger.debug("handler matched", i, mapper.expression, req.url); - - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type) { - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - return true; - } - } - - return false; - - } //end of handle - - }; - -}()); - - - - -/*ListDir.js*/ -/* -ListDir: List all the files in a directory -*/ -var ListDir = (function() { - - var urlFormat = function(url) { - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date) { - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num) { - return num + " ".substring(0, 12 - String(num).length); - }; - - //Create an anchor - var anchor = function(txt, url) { - url = url ? url : "/"; - return '' + txt + ""; - }; - - var listDir = { - //List all the files in a directory - list: function(req, res, dir) { - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function() { - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -        res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -      };
    -
    -      var listEnd = function() {
    -        res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files) { - if (err) { - listEnd(); - Logger.debug(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++) { - //Persistent the idx before make the sync process - (function(idx) { - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat) { - cur++; - - if (err) { - Logger.debug(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - } - }; - - return listDir; - -}()); -/*Template.js*/ -/* -* Templates -*/ -var Template = (function() { - - var engine = require("./lib/doT"); - - //get a file - var getFile = function(filename, cb){ - var fullpath = path.join(Settings.root, filename); - - fs.readFile(fullpath, function (err, html) { - err && Logger.debug(err); - err ? cb("") : cb(html); - }); - }; - - //render a file - var render = function(chrunk, params, outFn){ - try { - tmplFn = engine.compile(chrunk, params); - outFn(tmplFn(params)); - } catch(err) { - Logger.debug(err); - outFn(err); - } - }; - - return { - //render templates - render: function(chrunk, params) { - var res = this, - end = res.end; - - var url = chrunk.url, - con = chrunk.constructor; - - //It's a http request (it has "url") - if (url) { - getFile(url, function(tmpl) { - render(tmpl, params, end); - }); - - //It's html contents (template codes) - } else if (con == String) { - render(chrunk, params, end); - - //It's Array object (template file path) - } else if (con == Array) { - getFile(chrunk[0], function(tmpl) { - render(tmpl, params, end); - }); - - //Nothing matched end the response - } else { - end(); - } - - } - } - -}()); -/*Server.js*/ -/* -* Description: Create a Web Server -* Author: Kris Zhang -* Licenses: MIT, GPL -*/ -/* -* Define and Export WebSvr -*/ -var WebSvr = module.exports = (function() { - - var server = function(options) { - - //Parameters - //Count: How many files? - var self = this, - root, - port; - - var fileHandler = function(req, res) { - - var url = req.url, - hasQuery = url.indexOf("?"); - - //fs.stat can't recognize the file name with querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(root, url); - - //Handle path - var handlePath = function(phyPath) { - fs.stat(phyPath, function(err, stat) { - - //Consider as file not found - if (err) return self.write404(res); - - //Is file? Open this file and send to client. - if (stat.isFile()) { - // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 - var cacheTime = new Date(req.headers["if-modified-since"] || 1); - - // The file is modified - if (Settings.cache && stat.mtime <= cacheTime) { - res.writeHead(304); - res.end(); - - // Else send "not modifed" - } else { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, phyPath); - } - } - - //Is Directory? - else if (stat.isDirectory()) { - handleDefault(phyPath); - } - - //Or write the 404 pages - else { - self.write404(res); - } - - }); - }; - - //List all the files and folders. - var handleDir = function(dirPath) { - options.listDir - ? ListDir.list(req, res, dirPath) - : self.write403(res); - }; - - //Handle default page - var handleDefault = function(dirPath) { - var defaultPage = options.defaultPage; - - if (defaultPage) { - var defaultPath = path.join(dirPath, defaultPage); - - fs.exists(defaultPath, function (exists) { - //If page exists hanle it again - if (exists) { - //In order to make it as a dir path for loading static resources - if (url[url.length - 1] != '/') { - return res.redirect(url + '/'); - } - - handlePath(defaultPath); - //If page doesn't exist hanlde the dir again - } else { - handleDir(dirPath); - } - }); - } else { - handleDir(dirPath); - } - }; - - handlePath(fullPath); - }; - - var requestHandler = function(req, res) { - //Response may be shutdown when do the filter, in order not to cause exception, - //Rewrite the write/writeHead functionalities of current response object - var endFn = res.end; - res.end = function() { - //Execute old end - endFn.apply(res, arguments); - //Rewirte write/writeHead on response object - res.write = res.writeHead = res.setHeader = function() { - Logger.debug("response is already end, response.write ignored!") - }; - }; - - res.writeFile = function(filePath, cb) { - self.writeFile(res, filePath, cb); - }; - - //301/302 : move permanently - res.redirect = function(url, status) { - res.writeHead(status ? status : 302, { "Location": url }); - res.end(); - }; - - //render template objects - res.render = Template.render; - - //initial httprequest - var filterChain = new FilterChain(function(){ - - //if handler not match, send the request - !Handler.handle(req, res) && fileHandler(req, res); - - }, req, res); - - //Hook FilterChain object on the request - req.filter = filterChain; - - //Handle the first filter - req.filter.next(); - }; - - var writeFile = function(res, fullPath) { - fs.readFile(fullPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - } - res.setHeader("Content-Type", mime.lookup(fullPath)); - res.writeHead(200); - res.end(data, "binary"); - }); - }; - - //Explose API - //Filter - self.filter = Filter.filter; - self.file = Filter.file; - - //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; - - //Get a full path of a request - self.getFullPath = function(filePath) { - return path.join(root, filePath); - }; - - //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb) { - filePath = path.join(root, filePath); - fs.exists(filePath, function(exist) { - if (exist) { - writeFile(res, filePath); - cb && cb(exist); - }else{ - //If callback function doesn't exist, write 404 page; - cb ? cb(exist) : self.write404(res); - } - }); - }; - - self.write403 = function(res) { - res.writeHead(403, {"Content-Type": "text/html"}); - res.end("Access forbidden!"); - }; - - self.write404 = function(res) { - res.writeHead(404, {"Content-Type": "text/html"}); - res.end("File not found!"); - }; - - //Public: start http server - self.start = function() { - //Update the default value of Settings - options = _.extend(Settings, options); - - root = options.root; - port = parseInt(options.port); - - //Create http server - if (options.http) { - var httpSvr = http.createServer(requestHandler); - httpSvr.listen(port); - - Logger.log("Http server running at" - ,"Root:", root - ,"Port:", port - ); - - self.httpSvr = httpSvr; - } - - //Create https server - if (options.https) { - var httpsOpts = options.httpsOpts, - httpsPort = options.httpsPort; - - var httpsSvr = https.createServer(httpsOpts, requestHandler); - httpsSvr.listen(httpsPort); - - Logger.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); - - self.httpsSvr = httpsSvr; - } - - /* - init modules - */ - //Start session garbage collection - SessionManager.start(); - }; - - //Public: close http server; - self.close = function() { - self.httpSvr && self.httpSvr.close(); - self.httpsSvr && self.httpsSvr.close(); - }; - - }; - - return server; - -})(); +/* +* Description: node-websvr +* Author: Kris Zhang +* Licenses: MIT +* Project url: https://github.com/newghost/node-websvr +* +* Referenced projects: +* Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +* MIME: content-type in header + https://github.com/broofa/node-mime +* template: Template Engine + https://github.com/olado/doT +*/ + +//Node libraries +var fs = require("fs"); +var path = require("path"); +var qs = require("querystring"); +var os = require("os"); + +var http = require("http"); +var https = require("https"); + +//Open source libraries, some device may not have npm, so reference directly. +var mime = require("./lib/mime"); +var formidable = require('./lib/incoming_form'); + +/* +* Utility +*/ +var _ = { + //extend object to target + extend: function(tar, obj) { + if (!obj) return; + + for (var key in obj) { + tar[key] = obj[key]; + } + + return tar; + } +}; + + +/* +* Define and Export WebSvr +*/ +var WebSvr = module.exports = function(options) { + + var self = {}; + + /*****************Web module definitions*************/ + /* + Configurations + */ + var Settings = { + //root folder of web + root: "../" + + //http start + //default port of http + , port: 8054 + + //default port of https + , httpsPort: 8443 + , httpsKey: "" + , httpsCert: "" + + //list files in directory + , listDir: false + //enable client-side cache(304) + , cache: true + //enable debug information output + , debug: true + //receive buffer, default size 32k, etc: receive post data from ajax request + , bufferSize: 32768 + + //default pages, only one is supported + , defaultPage: "index.html" + + //logger file path + , logger: os.tmpDir() + "/log.txt" + + /* + Session timeout, in milliseconds. + When session is expired, session file will not deleted. + */ + , sessionTimeout: 1440000 + /* + Session garbage collection time, in milliseconds. + When session expired time is more than (sessionAge + sessionGCT), + then session file will be unlinked. + */ + , sessionGarbage: 3460000 + + //session file stored here + , sessionDir: os.tmpDir() + + //tempary upload file stored here + , uploadDir: os.tmpDir() + }; + + /* + Logger: log sth + */ + var Logger = (function() { + + var lineSeparator = "\r\n", + indentSeparator = "\t", + depth = 9; + + var write = function(logObj, dep) { + var depth = dep || depth, + output = new Date() + lineSeparator; + + function print(pre, obj) { + if (!obj) return; + for (var key in obj) { + var val = obj[key]; + output = output + pre + key + " : " + val + lineSeparator; + if (typeof val == "object") { + (pre.length < depth) && print(pre + indentSeparator, val); + } + } + } + + print(indentSeparator, logObj); + + fs.appendFile(Settings.logger, output, function(err) { + log(err); + }); + }; + + /* + Currnetly it's equal to console.log + */ + var log = function() { + console.log.apply(console, arguments); + }; + + /* + Add data before log information + */ + var debug = function() { + //diable console.log information + if (!Settings.debug) { + return; + } + + var d = new Date().toString(); + + Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + console.log.apply(console, arguments); + }; + + return { + log: log + , write: write + , debug: debug + }; + })(); + + /* + Body parser, parse the data in request body via + when parse complete, execute the callback, with response data; + */ + var BodyParser = function(req, res, callback) { + + var buffer = new Buffer(Settings.bufferSize); + + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + + /* + Parse request with session support + */ + var SessionParser = function(req, res) { + var self = this; + + //session id + self.sid = null; + //session stored object + self.obj = null; + //is this new session? + self.new = false; + + //init session object + self.init(req, res); + }; + + SessionParser.prototype = { + init: function(req, res) { + var self = this, + sidKey = "_wsid", + sidVal; + + //Get or Create sid, sid exist in the cookie, read it + var cookie = req.headers.cookie || ""; + var idx = cookie.indexOf(sidKey + "="); + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); + + //Sid doesn't exist, create it + if (idx < 0 || sidVal.length != 25) { + sidVal = SessionManager.create(); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + self.new = true; + }; + self.sid = sidVal; + + SessionManager.refresh(self.sid); + } + + //Create new session object + , newObj: function(key, cb) { + //Key is offered, return null of this key, else return empty session object + var self = this, + val = key ? null : {}; + + self.obj = {}; + cb && cb(val); + return val; + } + + //Get value from session object + , getVal: function(key, cb) { + var self = this; + + //key is null, return all the session object + var val = key ? self.obj[key] : self.obj; + cb && cb(val); + + return val; + } + + //Set an key/value pair in session object + , set: function(key, val, cb) { + var self = this; + + //Get session object first + self.get(function() { + + //Add or update key/value in session object + self.obj[key] = val; + + //Write or modify json file + fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { + if (err) { + Logger.debug(err); + return; + } + + cb && cb(self.obj); + + //force update + SessionManager.update(self.sid); + }); + }); + } + + //Get value from session file + , get: function(key, cb) { + var self = this; + + //The first parameter is callback function + if (key.constructor == Function) { + cb = key; + key = null; + } + + //The session object is already loaded + if (self.obj) return self.getVal(key, cb); + + //It's a new session file, need not to load it from file + if (self.new) return self.newObj(key, cb); + + var sessionPath = SessionManager.getPath(self.sid); + + //File operates, will cause delay + fs.exists(sessionPath, function(exists) { + //err: file doesn't exist + if (!exists) { + return self.newObj(key, cb); + + //session not expired + } else if (SessionManager.isValid(self.sid)) { + fs.readFile(sessionPath, function(err, data) { + if (err) { + Logger.debug(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + return self.getVal(key, cb); + }); + + //session expired, treat it as new session + } else { + return self.newObj(key, cb); + } + }); + } + }; + + /* + Parser: Functions that Filter and Handler will be called + */ + var Parser = function(req, res, mapper) { + + var handler = mapper.handler; + + //add sesion support + var parseSession = function() { + //add sesion support + if (mapper.session && typeof req.session == "undefined") { + req.session = new SessionParser(req, res); + } + handler(req, res); + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseBody = function() { + //need to parse the request? + if (mapper.parse && typeof req.body == "undefined") { + //Must parser the request first, or the post data will lost; + BodyParser(req, res, function(data) { + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function() { + //Need to parse the file in request? + if (mapper.file && typeof req.body == "undefined") { + //Must parser the request first, or the post data maybe lost; + var form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; + + form.parse(req, function(err, fields, files) { + if (err) { + Logger.debug(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseBody(); + }); + }else{ + parseBody(); + }; + }; + + parseFile(); + }; + + /* + SessionManager: + - Clear expired session files + - Valid session + */ + var SessionManager = (function() { + + //duration time + var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; + + //timer + var timer; + + //session array object, stored with {sid: [update time]}; + var list = {}; + + var getPath = function(sid) { + return path.join(Settings.sessionDir, sid); + }; + + //create a new session id + var create = function() { + //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions + var uuid + = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars + + '-' + + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars + + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars + + //fix the length to 25 + uuid += '00000000000000000000'.substr(0, 25 - uuid.length); + + return uuid; + }; + + //force update session in list + var update = function(sid, datetime) { + list[sid] = datetime || new Date(); + }; + + //remove a sesson from list + var remove = function(sid) { + //delete the file + fs.unlink(getPath(sid)); + //remove from list + delete list[sid]; + + Logger.debug("session removed", sid); + }; + + /* + Does session expired? + If the session is not in the list, add to the list. + i.e. When WebSvr restarted, session will not expired. + */ + var isValid = function(sid) { + var now = new Date(); + + !list[sid] && (list[sid] = now); + + return now - list[sid] <= Settings.sessionTimeout + }; + + /* + Session clean handler + */ + var cleanHandler = function() { + for (var sid in list) { + !isValid(sid) && remove(sid); + } + }; + + /* + Clean the session in temp folder + */ + var clean = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return Logger.debug(err); + + //converted to minutes + var expire = (+new Date() - gcTime) / 60000 | 0; + + files.forEach(function(file) { + if (file.length == 25) { + var stamp = parseInt(file.substr(0, file.indexOf('-'))); + + if (stamp) { + //remove the expired session + stamp < expire + ? remove(file) + : Logger.debug("session skipped", file); + } + } + }); + }); + }; + + //refresh session in list, valid first, if not expired, update the time + var refresh = function(sid, datetime) { + isValid(sid) && update(sid, datetime); + }; + + var stop = function() { + clearInterval(timer); + timer = null; + }; + + //stop before new session start + var start = function() { + //stop cleanHandler if available + stop(); + //clean the old sessions + clean(); + timer = setInterval(cleanHandler, gcTime); + }; + + return { + list: list, + create: create, + update: update, + remove: remove, + refresh: refresh, + isValid: isValid, + getPath: getPath, + clean: clean, + start: start, + stop: stop + } + })(); + + /* + Mapper: Used for Filter & Handler, + expression: required parameter + handler: required parameter + options: optional parameters + */ + var Mapper = function(expression, handler, options) { + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); + }; + + Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req) { + var self = this, + expression = self.expression; + + //No expression? It's a general filter mapper + if (!expression) return true; + + switch (expression.constructor) { + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options) { + for(key in options) { + this[key] = options[key] + } + } + }; + + /* + Http Filter: Execute all the rules that matched, + Filter will be always called before a handler. + */ + var Filter = { + //filter list + filters: [] + + /* + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] + */ + , filter: function(expression, handler, options) { + //The first parameter is Function => (handler, options) + if (expression.constructor == Function) { + options = handler; + handler = expression; + expression = null; + } + + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); + + return self; + } + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + , file: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, {file: true}); + //insert at the top of the filter array + Filter.filters.splice(0, 0, mapper); + + return self; + } + }; + + /* + Filter Chain + */ + var FilterChain = function(cb, req, res) { + var self = this; + + self.idx = 0; + self.cb = cb; + + self.req = req; + self.res = res; + }; + + FilterChain.prototype = { + next: function() { + var self = this, + req = self.req, + res = self.res; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if (!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res) { + //filter actions + req.next(req, res); + }, options); + */ + if (mapper.match(req)) { + Logger.debug("filter matched", self.idx, mapper.expression, req.url); + + //filter matched, parse the request and then execute it + Parser(req, res, mapper); + }else{ + //filter not matched, validate next filter + self.next(); + } + } + }; + + /* + Http Handler: Execute and returned when when first matched; + At the same time only one Handler will be called; + */ + var Handler = { + handlers: [] + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ + , url: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, options); + Handler.handlers.push(mapper); + + return self; + } + + //Post: Parse the post data by default; + , post: function(expression, handler, options) { + return this.url(expression, handler, _.extend({ parse: true }, options)); + } + + //Session: Parse the session and post by default; + , session: function(expression, handler) { + return this.url(expression, handler, { parse: true, session: true }); + } + + , handle: function(req, res) { + //flag: is matched? + for(var i = 0, len = Handler.handlers.length; i < len ; i++) { + + var mapper = Handler.handlers[i]; + if (mapper.match(req)) { + + Logger.debug("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type) { + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + return true; + } + } + + return false; + + } //end of handle + }; + + /* + ListDir: List all the files in a directory + */ + var ListDir = (function() { + + var urlFormat = function(url) { + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date) { + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num) { + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url) { + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list: function(req, res, dir) { + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function() { + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +        };
    +
    +        var listEnd = function() {
    +          res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files) { + if (err) { + listEnd(); + Logger.debug(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++) { + //Persistent the idx before make the sync process + (function(idx) { + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat) { + cur++; + + if (err) { + Logger.debug(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + }()); + + /* + * Templates + */ + var Template = (function() { + + var engine = require("./lib/doT"); + + //get a file + var getFile = function(filename, cb){ + var fullpath = path.join(Settings.root, filename); + + fs.readFile(fullpath, function (err, html) { + err && Logger.debug(err); + err ? cb("") : cb(html); + }); + }; + + //render a file + var render = function(chrunk, params, outFn){ + try { + tmplFn = engine.compile(chrunk, params); + outFn(tmplFn(params)); + } catch(err) { + Logger.debug(err); + outFn(err); + } + }; + + return { + //render templates + render: function(chrunk, params) { + var res = this, + end = res.end; + + var url = chrunk.url, + con = chrunk.constructor; + + //It's a http request (it has "url") + if (url) { + getFile(url, function(tmpl) { + render(tmpl, params, end); + }); + + //It's html contents (template codes) + } else if (con == String) { + render(chrunk, params, end); + + //It's Array object (template file path) + } else if (con == Array) { + getFile(chrunk[0], function(tmpl) { + render(tmpl, params, end); + }); + + //Nothing matched end the response + } else { + end(); + } + + } + } + }()); + + + /*****************Web initial codes*************/ + //Parameters + var root; + + var fileHandler = function(req, res) { + + var url = req.url, + hasQuery = url.indexOf("?"); + + //fs.stat can't recognize the file name with querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; + + var fullPath = path.join(root, url); + + //Handle path + var handlePath = function(phyPath) { + fs.stat(phyPath, function(err, stat) { + + //Consider as file not found + if (err) return self.write404(res); + + //Is file? Open this file and send to client. + if (stat.isFile()) { + // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 + var cacheTime = new Date(req.headers["if-modified-since"] || 1); + + // The file is modified + if (Settings.cache && stat.mtime <= cacheTime) { + res.writeHead(304); + res.end(); + + // Else send "not modifed" + } else { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, phyPath); + } + } + + //Is Directory? + else if (stat.isDirectory()) { + handleDefault(phyPath); + } + + //Or write the 404 pages + else { + self.write404(res); + } + + }); + }; + + //List all the files and folders. + var handleDir = function(dirPath) { + Settings.listDir + ? ListDir.list(req, res, dirPath) + : self.write403(res); + }; + + //Handle default page + var handleDefault = function(dirPath) { + var defaultPage = Settings.defaultPage; + + if (defaultPage) { + var defaultPath = path.join(dirPath, defaultPage); + + fs.exists(defaultPath, function (exists) { + //If page exists hanle it again + if (exists) { + //In order to make it as a dir path for loading static resources + if (url[url.length - 1] != '/') { + return res.redirect(url + '/'); + } + + handlePath(defaultPath); + //If page doesn't exist hanlde the dir again + } else { + handleDir(dirPath); + } + }); + } else { + handleDir(dirPath); + } + }; + + handlePath(fullPath); + }; + + var requestHandler = function(req, res) { + //Response may be shutdown when do the filter, in order not to cause exception, + //Rewrite the write/writeHead functionalities of current response object + var endFn = res.end; + res.end = function() { + //Execute old end + endFn.apply(res, arguments); + //Rewirte write/writeHead on response object + res.write = res.writeHead = res.setHeader = function() { + Logger.debug("response is already end, response.write ignored!") + }; + }; + + //relative path, relative to the web root + res.writeFile = function(filePath, cb) { + self.writeFile(res, filePath, cb); + }; + + //absolute path, relative to the server running + res.sendFile = function(filePath, cb) { + self.writeFile(res, filePath, cb, true) + }; + + //301/302 : move permanently + res.redirect = function(url, status) { + res.writeHead(status ? status : 302, { "Location": url }); + res.end(); + }; + + //render template objects + res.render = Template.render; + + //initial httprequest + var filterChain = new FilterChain(function(){ + + //if handler not match, send the request + !Handler.handle(req, res) && fileHandler(req, res); + + }, req, res); + + //Hook FilterChain object on the request + req.filter = filterChain; + + //Handle the first filter + req.filter.next(); + }; + + var writeFile = function(res, fullPath) { + fs.readFile(fullPath, function(err, data) { + if (err) { + Logger.debug(err); + return; + } + res.setHeader("Content-Type", mime.lookup(fullPath)); + res.writeHead(200); + res.end(data, "binary"); + }); + }; + + //API have function chain + //Filter + self.filter = Filter.filter; + self.file = Filter.file; + + //Handler + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; + + //Get a full path of a request + self.getFullPath = function(filePath) { + return path.join(root, filePath); + }; + + //Write file, filePath is relative path + self.writeFile = function(res, filePath, cb, isSvrPath) { + !isSvrPath && (filePath = path.join(root, filePath)); + fs.exists(filePath, function(exist) { + if (exist) { + writeFile(res, filePath); + cb && cb(exist); + }else{ + //If callback function doesn't exist, write 404 page; + cb ? cb(exist) : self.write404(res); + } + }); + + return self; + }; + + self.write403 = function(res) { + res.writeHead(403, {"Content-Type": "text/html"}); + res.end("Access forbidden!"); + + return self; + }; + + self.write404 = function(res) { + res.writeHead(404, {"Content-Type": "text/html"}); + res.end("File not found!"); + + return self; + }; + + //start http server + self.start = function() { + //Update the default value of Settings + _.extend(Settings, options); + + root = Settings.root; + + //Create http server + if (options.port) { + var port = Settings.port; + + var httpSvr = http.createServer(requestHandler); + httpSvr.listen(port); + + Logger.log("Http server running at" + ,"Root:", root + ,"Port:", port + ); + + self.httpSvr = httpSvr; + } + + //Create https server + if ( options.httpsPort + && options.httpsKey + && options.httpsCert) { + + var httpsPort = Settings.httpsPort; + + var httpsSvr = https.createServer({ + key: Settings.httpsKey, + cert: Settings.httpsCert + }, requestHandler).listen(httpsPort); + + Logger.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); + + self.httpsSvr = httpsSvr; + } + + /* + init modules + */ + //Start session garbage collection + SessionManager.start(); + + return self; + }; + + //stop http server + self.stop = function() { + self.httpSvr && self.httpSvr.close(); + self.httpsSvr && self.httpsSvr.close(); + SessionManager.stop(); + + return self; + }; + + //property: filters & handlers + Object.defineProperty(self, 'filters', { + get: function() { + return Filter.filters + }, + set: function(filters) { + Filter.filters = filters; + } + }); + + Object.defineProperty(self, 'handlers', { + get: function() { + return Handler.handlers; + }, + set: function(handlers) { + Handler.handlers = handlers; + } + }) + + return self; + +}; From fe12ae576869dedcfbc11e57adc3f7d7bee2aaf3 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 16 Apr 2013 23:51:19 +0800 Subject: [PATCH 064/195] Update demo Update demo and default settings of http & https --- website/svr/sitetest.js | 282 ++++++++++++++++++++-------------------- websvr/websvr.js | 6 +- 2 files changed, 147 insertions(+), 141 deletions(-) diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index eed79d0..9f692ad 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -1,138 +1,144 @@ -//import namespace -var WebSvr = require("./../../websvr/websvr.js"); - -//Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; -//Trying at: http://localhost:8054 -var webSvr = new WebSvr({ - root: "./", - - //enable https - https: true, - //default port of https - httpsPort: 8443, - httpsOpts: { - key: require("fs").readFileSync("svr/cert/privatekey.pem"), - cert: require("fs").readFileSync("svr/cert/certificate.pem") - }, - - //defaultPage: "index.htm", - - //Change the default locations of tmp session and upload files - //session file stored here, must be end with "/" - sessionDir: "tmp/session/", - //tempary upload file stored here, must be end with "/" - uploadDir: "tmp/upload/", - - listDir: true, - debug: true -}); - -webSvr.start(); - -/* -General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; - session: init the session and stored in req.session; -*/ -webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); - - //Link to next filter - req.filter.next(); -}, {parse:true, session:true}); - -/* -Session Filter: protect web/* folder => (validation by session); -*/ -webSvr.filter(/web\/[\w\.]+/, function(req, res) { - //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - //Once session is get initialized - //TODO: Make sure next req.session.get() will not load session file again. - req.session.get("username", function(val) { - console.log("session username:", val); - - !val && res.end("You must login, first!"); - - //Link to next filter - req.filter.next(); - }); - - } else { - req.filter.next(); - } -}); - - -/* -Handler: login.do => (validate the username & password) - username: admin - password: 12345678 -*/ -webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); - if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session) { - //res.writeHead(200, {"Content-Type": "text/html"}); - //res.writeFile("/web/setting.htm"); - //TODO: Error handler of undefined methods - console.log(session); - res.redirect("/web/setting.htm"); - }); - } else { - res.writeHead(401); - res.end("Wrong username/password"); - } -}); - -/* -Uploader: upload.do => (receive handler) -*/ -webSvr.file("upload.do", function(req, res) { - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); -}); - -/* -Redirect: redirect request, try at: http://localhost:8054/redirect -*/ -webSvr.url("redirect", function(req, res) { - res.redirect("/svr/websvr.all.js"); -}); - -/* -Template: render template with params -*/ -webSvr.url("template.node", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //render template with session: { "username" : "admin" } - req.session.get(function(session) { - //TODO: Change to req.render(session); - res.render(req, session); - }); -}); - -/* -Simple redirect API: -*/ -//Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine -webSvr.url("combine", ["svr/tool/Combine.js"]); -//Mapping "hello" to a string, trying at http://localhost:8054/hello -webSvr.url("hello", "Hello WebSvr!"); -//Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm -webSvr.post("post.htm", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //With session support: "{session: true}" - res.write("You username is " + req.session.get("username")); - res.write('
    '); - res.end('Received : ' + req.body); -}, {session: true}); \ No newline at end of file +//import namespace +var WebSvr = require("./../../websvr/websvr.js"); + +//Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; +//Trying at: http://localhost:8054 +var webSvr = new WebSvr({ + root: "./" + , listDir: true + , debug: true +}).start(); + +/* +General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; + session: init the session and stored in req.session; +*/ +webSvr.filter(function(req, res) { + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(); +}, {parse:true, session:true}); + +/* +Session Filter: protect web/* folder => (validation by session); +*/ +webSvr.filter(/web\/[\w\.]+/, function(req, res) { + //It's not index.htm/login.do, do the session validation + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { + //Once session is get initialized + //TODO: Make sure next req.session.get() will not load session file again. + req.session.get("username", function(val) { + console.log("session username:", val); + + !val && res.end("You must login, first!"); + + //Link to next filter + req.filter.next(); + }); + + } else { + req.filter.next(); + } +}); + + +/* +Handler: login.do => (validate the username & password) + username: admin + password: 12345678 +*/ +webSvr.session("login.do", function(req, res) { + var querystring = require("querystring"); + + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if (qs.username == "admin" && qs.password == "12345678") { + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session) { + //res.writeHead(200, {"Content-Type": "text/html"}); + //res.writeFile("/web/setting.htm"); + //TODO: Error handler of undefined methods + console.log(session); + res.redirect("/web/setting.htm"); + }); + } else { + res.writeHead(401); + res.end("Wrong username/password"); + } +}); + +/* +Uploader: upload.do => (receive handler) +*/ +webSvr.file("upload.do", function(req, res) { + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); +}); + +/* +Redirect: redirect request, try at: http://localhost:8054/redirect +*/ +webSvr.url("redirect", function(req, res) { + res.redirect("/svr/websvr.all.js"); +}); + +/* +Template: render template with params +*/ +webSvr.url("template.node", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //render template with session: { "username" : "admin" } + req.session.get(function(session) { + //TODO: Change to req.render(session); + res.render(req, session); + }); +}); + +/* +Simple redirect API: +*/ +webSvr + //Mapping "sitest" to tool/Combine.js, trying at: http://localhost:8054/combine + .url("sitetest", ["svr/sitetest.js"]) + //Mapping "hello" to a string, trying at http://localhost:8054/hello + .url("hello", "Hello WebSvr!") + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm + .post("post.htm", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //With session support: "{session: true}" + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); + }, {session: true}); + + +var httpsSvr = new WebSvr({ + root: "./" + + //disable http server + , port: null + + //enable https server + , httpsPort: 8443 + , httpsKey: require("fs").readFileSync("svr/cert/privatekey.pem") + , httpsCert: require("fs").readFileSync("svr/cert/certificate.pem") + + //, defaultPage: "index.htm" + , listDir: true + + //Change the default locations of tmp session and upload files + //session file stored here + , sessionDir: "tmp/session/" + //tempary upload file stored here + , uploadDir: "tmp/upload/" +}).start(); + +httpsSvr.filters = webSvr.filters; +httpsSvr.handlers = webSvr.handlers; \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index bf52590..37d10ae 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1065,8 +1065,8 @@ var WebSvr = module.exports = function(options) { root = Settings.root; - //Create http server - if (options.port) { + //Create http server: Enable by default + if (Settings.port) { var port = Settings.port; var httpSvr = http.createServer(requestHandler); @@ -1080,7 +1080,7 @@ var WebSvr = module.exports = function(options) { self.httpSvr = httpSvr; } - //Create https server + //Create https server: Disable by default if ( options.httpsPort && options.httpsKey && options.httpsCert) { From e0dfd91e8a4aa01f4a4c67b048ee411f21f785a2 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 17 Apr 2013 00:17:17 +0800 Subject: [PATCH 065/195] Update README --- README.md | 337 ++++++++++++++++++++-------------------- website/svr/sitetest.js | 2 +- 2 files changed, 173 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 5152012..eaa9629 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,172 @@ -WebSvr -============== -A simple web server based on node.js -Lincenses: MIT, GPL -Version: 0.0.4 - -Features --------------- -- Filter: A request will try to match all the filters first, and then pass to the Handler -- Handler: When a request matched a handler, it will returned, only one handler will be executed -- Session: Stored in file, with JSON format -- File: Support uploading files -- TODO: Custom index page and 404 error pages - -Start --------------- -Edit in SiteTest.js or Create a new Site.js and added to MakeFile.list - - //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; - //Trying at: http://localhost:8054 - var webSvr = new WebSvr({ - root: "./", - - //enable https - https: true, - //default port of https - httpsPort: 8443, - httpsOpts: { - key: require("fs").readFileSync("svr/cert/privatekey.pem"), - cert: require("fs").readFileSync("svr/cert/certificate.pem") - }, - - //Change the default locations of tmp session and upload files - //session file stored here, must be end with "/" - sessionDir: "tmp/session/", - //tempary upload file stored here, must be end with "/" - uploadDir: "tmp/upload/", - - - listDir: true, - debug: true - }); - - webSvr.start(); - -Filter --------------- -Session based authentication (session stored in files), all the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" - - /* - General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; - session: init the session and stored in req.session; - */ - webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); - - //Link to next filter - req.filter.next(); - }, {parse:true, session:true}); - - /* - Session Filter: protect web/* folder => (validation by session); - */ - webSvr.filter(/web\/[\w\.]+/, function(req, res) { - //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - req.session.get("username", function(val){ - console.log("session", val); - - !val && res.end("You must login, first!"); - }); - } - - //Link to next filter - req.filter.next(); - }); - - -Handler --------------- -Handle Login and put the username in Session - - /* - Handler: login.do => (validate the username & password) - username: admin - password: 12345678 - */ - webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); - if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session) { - //res.writeHead(200, {"Content-Type": "text/html"}); - //res.writeFile("/web/setting.htm"); - //TODO: Error handler of undefined methods - console.log(session); - res.redirect("/web/setting.htm"); - }); - }else{ - res.writeHead(401); - res.end("Wrong username/password"); - } - }); - -File --------------- -Receive upload file (it's a specfic filter) - - /* - Uploader: upload.do => (receive handler) - */ - webSvr.file("upload.do", function(req, res) { - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); - }); - -Template --------------- -Render template with params - - webSvr.url("template.node", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //render template with session: { "username" : "admin" } - req.session.get(function(session) { - res.render(req, session); - }); - }); - -Other APIs --------------- -Redirect - - /* - Redirect: redirect request, try at: http://localhost:8054/redirect - */ - webSvr.url("redirect", function(req, res){ - res.redirect("/svr/websvr.all.js"); - }); - -Url Mapping - - //Mapping "combine" to tool/Combine.js, trying at: http://localhost:8054/combine - webSvr.url(/combine/, ["svr/tool/Combine.js"]); - //Mapping "hello" to a string, trying at http://localhost:8054/hello - webSvr.url(/hello/, "Hello WebSvr!"); - -Post Data - - //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm - webSvr.post(/post.htm/, function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //Need session support - res.write("You username is " + req.session.get("username")); - res.write('

    '); - res.end('Received : ' + req.body); - }, {session: true}); \ No newline at end of file +WebSvr +============== +A simple web server based on node.js. +Lincenses: MIT + +Features +-------------- +- Designed for ARM: We need to keep embed device running stable in a very long time(> 1 year?), it may run into issues but it can restart and re-covery the user sessions automatically. +- Filter: A request will try to match all the filters first, and then pass to the Handler +- Handler: When a request matched a handler, it will returned, only one handler will be executed +- Session: Stored in file, with JSON format +- File: Support uploading files +- Custom index pages + +Start +-------------- +It's simple to start the websvr. + + //import WebSvr module, assume we don't have NPM. + var WebSvr = require("./../../websvr/websvr.js"); + + //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; + //Trying at: http://localhost:8054 + var webSvr = new WebSvr({ + root: "./" + , listDir: true + , debug: true + }).start(); + + +Filter +-------------- +Session based authentication, basically useage: + + /* + General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; + session: init the session and stored in req.session; + */ + webSvr.filter(function(req, res) { + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(); + }, {parse:true, session:true}); + +Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" + + /* + Session Filter: protect web/* folder => (validation by session); + */ + webSvr.filter(/web\/[\w\.]+/, function(req, res) { + //It's not index.htm/login.do, do the session validation + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { + req.session.get("username", function(val){ + console.log("session", val); + + !val && res.end("You must login, first!"); + }); + } + + //Link to next filter + req.filter.next(); + }); + + +Handler +-------------- +Handle Login and put the username in Session + + /* + Handler: login.do => (validate the username & password) + username: admin + password: 12345678 + */ + webSvr.session("login.do", function(req, res) { + var querystring = require("querystring"); + + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if (qs.username == "admin" && qs.password == "12345678") { + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session) { + //res.writeHead(200, {"Content-Type": "text/html"}); + //res.writeFile("/web/setting.htm"); + //TODO: Error handler of undefined methods + console.log(session); + res.redirect("/web/setting.htm"); + }); + }else{ + res.writeHead(401); + res.end("Wrong username/password"); + } + }); + +File +-------------- +Receive upload file (it's a specfic filter) + + /* + Uploader: upload.do => (receive handler) + */ + webSvr.file("upload.do", function(req, res) { + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); + }); + +Template +-------------- +Render template with params, using doT template engine + + webSvr.url("template.node", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //render template with session: { "username" : "admin" } + req.session.get(function(session) { + res.render(req, session); + }); + }); + +Other APIs +-------------- +Redirect, Url Mapping, Post Data + + webSvr + //Mapping "sitest" to tool/Combine.js, trying at: http://localhost:8054/combine + .url("sitetest", ["svr/sitetest.js"]) + //Mapping "hello" to a string, trying at http://localhost:8054/hello + .url("hello", "Hello WebSvr!") + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm + .post("post.htm", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //With session support: "{session: true}" + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); + }, {session: true}); + + +Multi-instance support +-------------- +Start a https server, make sure that the port will no conflict with others. + + var httpsSvr = new WebSvr({ + root: "./" + + //disable http server + , port: null + + //enable https server + , httpsPort: 8443 + , httpsKey: require("fs").readFileSync("svr/cert/privatekey.pem") + , httpsCert: require("fs").readFileSync("svr/cert/certificate.pem") + + //, defaultPage: "index.htm" + , listDir: true + + //Change the default locations of tmp session and upload files + //session file stored here + , sessionDir: "tmp/session/" + //tempary upload file stored here + , uploadDir: "tmp/upload/" + }).start(); + +Do you want to re-use the filters & handlers? + + httpsSvr.filters = webSvr.filters; + httpsSvr.handlers = webSvr.handlers; \ No newline at end of file diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 9f692ad..0ade6ae 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -1,4 +1,4 @@ -//import namespace +//import WebSvr module, assume we don't have NPM. var WebSvr = require("./../../websvr/websvr.js"); //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; From d66de6d4bc018b77853a9b8974396654bbb951aa Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 17 Apr 2013 00:20:49 +0800 Subject: [PATCH 066/195] Update README. --- README.md | 376 +++++++++++++++++++++++++++++------------------------- 1 file changed, 204 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index eaa9629..6a5985d 100644 --- a/README.md +++ b/README.md @@ -1,172 +1,204 @@ -WebSvr -============== -A simple web server based on node.js. -Lincenses: MIT - -Features --------------- -- Designed for ARM: We need to keep embed device running stable in a very long time(> 1 year?), it may run into issues but it can restart and re-covery the user sessions automatically. -- Filter: A request will try to match all the filters first, and then pass to the Handler -- Handler: When a request matched a handler, it will returned, only one handler will be executed -- Session: Stored in file, with JSON format -- File: Support uploading files -- Custom index pages - -Start --------------- -It's simple to start the websvr. - - //import WebSvr module, assume we don't have NPM. - var WebSvr = require("./../../websvr/websvr.js"); - - //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; - //Trying at: http://localhost:8054 - var webSvr = new WebSvr({ - root: "./" - , listDir: true - , debug: true - }).start(); - - -Filter --------------- -Session based authentication, basically useage: - - /* - General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; - session: init the session and stored in req.session; - */ - webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); - - //Link to next filter - req.filter.next(); - }, {parse:true, session:true}); - -Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" - - /* - Session Filter: protect web/* folder => (validation by session); - */ - webSvr.filter(/web\/[\w\.]+/, function(req, res) { - //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - req.session.get("username", function(val){ - console.log("session", val); - - !val && res.end("You must login, first!"); - }); - } - - //Link to next filter - req.filter.next(); - }); - - -Handler --------------- -Handle Login and put the username in Session - - /* - Handler: login.do => (validate the username & password) - username: admin - password: 12345678 - */ - webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); - if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session) { - //res.writeHead(200, {"Content-Type": "text/html"}); - //res.writeFile("/web/setting.htm"); - //TODO: Error handler of undefined methods - console.log(session); - res.redirect("/web/setting.htm"); - }); - }else{ - res.writeHead(401); - res.end("Wrong username/password"); - } - }); - -File --------------- -Receive upload file (it's a specfic filter) - - /* - Uploader: upload.do => (receive handler) - */ - webSvr.file("upload.do", function(req, res) { - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); - }); - -Template --------------- -Render template with params, using doT template engine - - webSvr.url("template.node", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //render template with session: { "username" : "admin" } - req.session.get(function(session) { - res.render(req, session); - }); - }); - -Other APIs --------------- -Redirect, Url Mapping, Post Data - - webSvr - //Mapping "sitest" to tool/Combine.js, trying at: http://localhost:8054/combine - .url("sitetest", ["svr/sitetest.js"]) - //Mapping "hello" to a string, trying at http://localhost:8054/hello - .url("hello", "Hello WebSvr!") - //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm - .post("post.htm", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //With session support: "{session: true}" - res.write("You username is " + req.session.get("username")); - res.write('

    '); - res.end('Received : ' + req.body); - }, {session: true}); - - -Multi-instance support --------------- -Start a https server, make sure that the port will no conflict with others. - - var httpsSvr = new WebSvr({ - root: "./" - - //disable http server - , port: null - - //enable https server - , httpsPort: 8443 - , httpsKey: require("fs").readFileSync("svr/cert/privatekey.pem") - , httpsCert: require("fs").readFileSync("svr/cert/certificate.pem") - - //, defaultPage: "index.htm" - , listDir: true - - //Change the default locations of tmp session and upload files - //session file stored here - , sessionDir: "tmp/session/" - //tempary upload file stored here - , uploadDir: "tmp/upload/" - }).start(); - -Do you want to re-use the filters & handlers? - - httpsSvr.filters = webSvr.filters; - httpsSvr.handlers = webSvr.handlers; \ No newline at end of file +WebSvr +============== +A simple web server based on node.js. +Lincenses: MIT + +Features +-------------- +- Designed for ARM: We need to keep embed device running stable in a very long time(> 1 year?), it may run into issues but it can restart and re-covery the user sessions automatically. +- Filter: A request will try to match all the filters first, and then pass to the Handler +- Handler: When a request matched a handler, it will returned, only one handler will be executed +- Session: Stored in file, with JSON format +- File: Support uploading files +- Custom index pages + +Start +-------------- +It's simple to start the websvr. + + //import WebSvr module, assume we don't have NPM. + var WebSvr = require("./../../websvr/websvr.js"); + + //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; + //Trying at: http://localhost:8054 + var webSvr = new WebSvr({ + root: "./" + , listDir: true + , debug: true + }).start(); + + +Filter +-------------- +Session based authentication, basically useage: + + /* + General filter: parse the post data / session before all request + parse: parse the post data and stored in req.body; + session: init the session and stored in req.session; + */ + webSvr.filter(function(req, res) { + //TODO: Add greeting words in filter + //res.write("Hello WebSvr!
    "); + + //Link to next filter + req.filter.next(); + }, {parse:true, session:true}); + +Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" + + /* + Session Filter: protect web/* folder => (validation by session); + */ + webSvr.filter(/web\/[\w\.]+/, function(req, res) { + //It's not index.htm/login.do, do the session validation + if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { + req.session.get("username", function(val){ + console.log("session", val); + + !val && res.end("You must login, first!"); + }); + } + + //Link to next filter + req.filter.next(); + }); + + +Handler +-------------- +Handle Login and put the username in Session + + /* + Handler: login.do => (validate the username & password) + username: admin + password: 12345678 + */ + webSvr.session("login.do", function(req, res) { + var querystring = require("querystring"); + + //TODO: Add an parameter to auto-complete querystring.parse(req.body); + var qs = querystring.parse(req.body); + if (qs.username == "admin" && qs.password == "12345678") { + //Put key/value pair in session + //TODO: Support put JSON object directly + req.session.set("username", qs.username, function(session) { + //res.writeHead(200, {"Content-Type": "text/html"}); + //res.writeFile("/web/setting.htm"); + //TODO: Error handler of undefined methods + console.log(session); + res.redirect("/web/setting.htm"); + }); + }else{ + res.writeHead(401); + res.end("Wrong username/password"); + } + }); + +File +-------------- +Receive upload file (it's a specfic filter) + + /* + Uploader: upload.do => (receive handler) + */ + webSvr.file("upload.do", function(req, res) { + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); + }); + +Template +-------------- +Render template with params, using doT template engine + + webSvr.url("template.node", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //render template with session: { "username" : "admin" } + req.session.get(function(session) { + res.render(req, session); + }); + }); + +Other APIs +-------------- +Redirect, Url Mapping, Post Data + + webSvr + //Mapping "sitest" to tool/Combine.js, trying at: http://localhost:8054/combine + .url("sitetest", ["svr/sitetest.js"]) + //Mapping "hello" to a string, trying at http://localhost:8054/hello + .url("hello", "Hello WebSvr!") + //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm + .post("post.htm", function(req, res) { + res.writeHead(200, {"Content-Type": "text/html"}); + //With session support: "{session: true}" + res.write("You username is " + req.session.get("username")); + res.write('

    '); + res.end('Received : ' + req.body); + }, {session: true}); + + +Multi-instance support +-------------- +Start a https server, make sure that the port will no conflict with others. + + var httpsSvr = new WebSvr({ + root: "./" + + //disable http server + , port: null + + //enable https server + , httpsPort: 8443 + , httpsKey: require("fs").readFileSync("svr/cert/privatekey.pem") + , httpsCert: require("fs").readFileSync("svr/cert/certificate.pem") + + //, defaultPage: "index.htm" + , listDir: true + + //Change the default locations of tmp session and upload files + //session file stored here + , sessionDir: "tmp/session/" + //tempary upload file stored here + , uploadDir: "tmp/upload/" + }).start(); + +Do you want to re-use the filters & handlers? + + httpsSvr.filters = webSvr.filters; + httpsSvr.handlers = webSvr.handlers; + + + + + + + + + + + + + + + + + + + + + + + + + + + + +node-websvr +==== +基äºNodeJS的一个æç®€WebæœåС噍, 专为ARM设计。 +å‡è®¾åµŒå…¥å¼è®¾å¤‡éœ€è¦ä¿æŒé•¿æ—¶é—´ç¨³å®šè¿è¡Œï¼Œå½“é‡åˆ°é—®é¢˜æ—¶ä¹Ÿå¯è‡ªåЍé‡å¯å¹¶æ¢å¤æ­¤å‰ç”¨æˆ·çš„Session会è¯ã€‚ \ No newline at end of file From f855fe4f79e25da8e19ba8600b4fc74427163a00 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 17 Apr 2013 00:22:18 +0800 Subject: [PATCH 067/195] Update README --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6a5985d..0ee6329 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,16 @@ Start -------------- It's simple to start the websvr. - //import WebSvr module, assume we don't have NPM. - var WebSvr = require("./../../websvr/websvr.js"); - - //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; - //Trying at: http://localhost:8054 - var webSvr = new WebSvr({ - root: "./" - , listDir: true - , debug: true - }).start(); + //import WebSvr module, assume we don't have NPM. + var WebSvr = require("./../../websvr/websvr.js"); + + //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; + //Trying at: http://localhost:8054 + var webSvr = new WebSvr({ + root: "./" + , listDir: true + , debug: true + }).start(); Filter From 4361b969d07e30cb0a81296a54be611e14a5afc8 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 17 Apr 2013 23:14:36 +0800 Subject: [PATCH 068/195] Update shortcuts --- websvr/websvr.js | 2287 +++++++++++++++++++++++----------------------- 1 file changed, 1145 insertions(+), 1142 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 37d10ae..e9323be 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1,1142 +1,1145 @@ -/* -* Description: node-websvr -* Author: Kris Zhang -* Licenses: MIT -* Project url: https://github.com/newghost/node-websvr -* -* Referenced projects: -* Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -* MIME: content-type in header - https://github.com/broofa/node-mime -* template: Template Engine - https://github.com/olado/doT -*/ - -//Node libraries -var fs = require("fs"); -var path = require("path"); -var qs = require("querystring"); -var os = require("os"); - -var http = require("http"); -var https = require("https"); - -//Open source libraries, some device may not have npm, so reference directly. -var mime = require("./lib/mime"); -var formidable = require('./lib/incoming_form'); - -/* -* Utility -*/ -var _ = { - //extend object to target - extend: function(tar, obj) { - if (!obj) return; - - for (var key in obj) { - tar[key] = obj[key]; - } - - return tar; - } -}; - - -/* -* Define and Export WebSvr -*/ -var WebSvr = module.exports = function(options) { - - var self = {}; - - /*****************Web module definitions*************/ - /* - Configurations - */ - var Settings = { - //root folder of web - root: "../" - - //http start - //default port of http - , port: 8054 - - //default port of https - , httpsPort: 8443 - , httpsKey: "" - , httpsCert: "" - - //list files in directory - , listDir: false - //enable client-side cache(304) - , cache: true - //enable debug information output - , debug: true - //receive buffer, default size 32k, etc: receive post data from ajax request - , bufferSize: 32768 - - //default pages, only one is supported - , defaultPage: "index.html" - - //logger file path - , logger: os.tmpDir() + "/log.txt" - - /* - Session timeout, in milliseconds. - When session is expired, session file will not deleted. - */ - , sessionTimeout: 1440000 - /* - Session garbage collection time, in milliseconds. - When session expired time is more than (sessionAge + sessionGCT), - then session file will be unlinked. - */ - , sessionGarbage: 3460000 - - //session file stored here - , sessionDir: os.tmpDir() - - //tempary upload file stored here - , uploadDir: os.tmpDir() - }; - - /* - Logger: log sth - */ - var Logger = (function() { - - var lineSeparator = "\r\n", - indentSeparator = "\t", - depth = 9; - - var write = function(logObj, dep) { - var depth = dep || depth, - output = new Date() + lineSeparator; - - function print(pre, obj) { - if (!obj) return; - for (var key in obj) { - var val = obj[key]; - output = output + pre + key + " : " + val + lineSeparator; - if (typeof val == "object") { - (pre.length < depth) && print(pre + indentSeparator, val); - } - } - } - - print(indentSeparator, logObj); - - fs.appendFile(Settings.logger, output, function(err) { - log(err); - }); - }; - - /* - Currnetly it's equal to console.log - */ - var log = function() { - console.log.apply(console, arguments); - }; - - /* - Add data before log information - */ - var debug = function() { - //diable console.log information - if (!Settings.debug) { - return; - } - - var d = new Date().toString(); - - Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); - console.log.apply(console, arguments); - }; - - return { - log: log - , write: write - , debug: debug - }; - })(); - - /* - Body parser, parse the data in request body via - when parse complete, execute the callback, with response data; - */ - var BodyParser = function(req, res, callback) { - - var buffer = new Buffer(Settings.bufferSize); - - var length = 0, data = ""; - - req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; - }); - - req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); - }); - }; - - /* - Parse request with session support - */ - var SessionParser = function(req, res) { - var self = this; - - //session id - self.sid = null; - //session stored object - self.obj = null; - //is this new session? - self.new = false; - - //init session object - self.init(req, res); - }; - - SessionParser.prototype = { - init: function(req, res) { - var self = this, - sidKey = "_wsid", - sidVal; - - //Get or Create sid, sid exist in the cookie, read it - var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); - - //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 25) { - sidVal = SessionManager.create(); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - self.new = true; - }; - self.sid = sidVal; - - SessionManager.refresh(self.sid); - } - - //Create new session object - , newObj: function(key, cb) { - //Key is offered, return null of this key, else return empty session object - var self = this, - val = key ? null : {}; - - self.obj = {}; - cb && cb(val); - return val; - } - - //Get value from session object - , getVal: function(key, cb) { - var self = this; - - //key is null, return all the session object - var val = key ? self.obj[key] : self.obj; - cb && cb(val); - - return val; - } - - //Set an key/value pair in session object - , set: function(key, val, cb) { - var self = this; - - //Get session object first - self.get(function() { - - //Add or update key/value in session object - self.obj[key] = val; - - //Write or modify json file - fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { - if (err) { - Logger.debug(err); - return; - } - - cb && cb(self.obj); - - //force update - SessionManager.update(self.sid); - }); - }); - } - - //Get value from session file - , get: function(key, cb) { - var self = this; - - //The first parameter is callback function - if (key.constructor == Function) { - cb = key; - key = null; - } - - //The session object is already loaded - if (self.obj) return self.getVal(key, cb); - - //It's a new session file, need not to load it from file - if (self.new) return self.newObj(key, cb); - - var sessionPath = SessionManager.getPath(self.sid); - - //File operates, will cause delay - fs.exists(sessionPath, function(exists) { - //err: file doesn't exist - if (!exists) { - return self.newObj(key, cb); - - //session not expired - } else if (SessionManager.isValid(self.sid)) { - fs.readFile(sessionPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - return self.getVal(key, cb); - }); - - //session expired, treat it as new session - } else { - return self.newObj(key, cb); - } - }); - } - }; - - /* - Parser: Functions that Filter and Handler will be called - */ - var Parser = function(req, res, mapper) { - - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - req.session = new SessionParser(req, res); - } - handler(req, res); - }; - - /* - parse data in request, this should be done before parse session, - because session stored in file - */ - var parseBody = function() { - //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { - //Must parser the request first, or the post data will lost; - BodyParser(req, res, function(data) { - req.body = data; - parseSession(); - }); - }else{ - parseSession(); - } - }; - - /* - parse file in request, this should be at the top of the list - */ - var parseFile = function() { - //Need to parse the file in request? - if (mapper.file && typeof req.body == "undefined") { - //Must parser the request first, or the post data maybe lost; - var form = new formidable.IncomingForm(); - - form.uploadDir = Settings.uploadDir; - - form.parse(req, function(err, fields, files) { - if (err) { - Logger.debug(err); - return; - }; - - //attach the parameters and files - req.body = fields; - req.files = files; - - //in fact request will not be parsed again, because body is not undefined - parseBody(); - }); - }else{ - parseBody(); - }; - }; - - parseFile(); - }; - - /* - SessionManager: - - Clear expired session files - - Valid session - */ - var SessionManager = (function() { - - //duration time - var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; - - //timer - var timer; - - //session array object, stored with {sid: [update time]}; - var list = {}; - - var getPath = function(sid) { - return path.join(Settings.sessionDir, sid); - }; - - //create a new session id - var create = function() { - //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions - var uuid - = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars - + '-' - + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars - + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars - - //fix the length to 25 - uuid += '00000000000000000000'.substr(0, 25 - uuid.length); - - return uuid; - }; - - //force update session in list - var update = function(sid, datetime) { - list[sid] = datetime || new Date(); - }; - - //remove a sesson from list - var remove = function(sid) { - //delete the file - fs.unlink(getPath(sid)); - //remove from list - delete list[sid]; - - Logger.debug("session removed", sid); - }; - - /* - Does session expired? - If the session is not in the list, add to the list. - i.e. When WebSvr restarted, session will not expired. - */ - var isValid = function(sid) { - var now = new Date(); - - !list[sid] && (list[sid] = now); - - return now - list[sid] <= Settings.sessionTimeout - }; - - /* - Session clean handler - */ - var cleanHandler = function() { - for (var sid in list) { - !isValid(sid) && remove(sid); - } - }; - - /* - Clean the session in temp folder - */ - var clean = function() { - fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.debug(err); - - //converted to minutes - var expire = (+new Date() - gcTime) / 60000 | 0; - - files.forEach(function(file) { - if (file.length == 25) { - var stamp = parseInt(file.substr(0, file.indexOf('-'))); - - if (stamp) { - //remove the expired session - stamp < expire - ? remove(file) - : Logger.debug("session skipped", file); - } - } - }); - }); - }; - - //refresh session in list, valid first, if not expired, update the time - var refresh = function(sid, datetime) { - isValid(sid) && update(sid, datetime); - }; - - var stop = function() { - clearInterval(timer); - timer = null; - }; - - //stop before new session start - var start = function() { - //stop cleanHandler if available - stop(); - //clean the old sessions - clean(); - timer = setInterval(cleanHandler, gcTime); - }; - - return { - list: list, - create: create, - update: update, - remove: remove, - refresh: refresh, - isValid: isValid, - getPath: getPath, - clean: clean, - start: start, - stop: stop - } - })(); - - /* - Mapper: Used for Filter & Handler, - expression: required parameter - handler: required parameter - options: optional parameters - */ - var Mapper = function(expression, handler, options) { - var self = this; - - self.expression = expression; - self.handler = handler; - - //Has other parameters? - self.extend(options); - }; - - Mapper.prototype = { - /* - Does this mapper matched this request? - */ - match: function(req) { - var self = this, - expression = self.expression; - - //No expression? It's a general filter mapper - if (!expression) return true; - - switch (expression.constructor) { - case String: return req.url.indexOf(expression) > -1; - case RegExp: return expression.test(req.url); - } - - return false; - }, - - /* - Add optional parameters on current mapper - i.e: - session: boolean - file: boolean - parse: boolean - */ - extend: function(options) { - for(key in options) { - this[key] = options[key] - } - } - }; - - /* - Http Filter: Execute all the rules that matched, - Filter will be always called before a handler. - */ - var Filter = { - //filter list - filters: [] - - /* - filter: add a new filter - expression: string/regexp [optional] - handler: function [required] - options: object [optional] - */ - , filter: function(expression, handler, options) { - //The first parameter is Function => (handler, options) - if (expression.constructor == Function) { - options = handler; - handler = expression; - expression = null; - } - - var mapper = new Mapper(expression, handler, options); - Filter.filters.push(mapper); - - return self; - } - - /* - file receiver: it's a specfic filter, - this filter should be always at the top of the filter list - */ - , file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); - //insert at the top of the filter array - Filter.filters.splice(0, 0, mapper); - - return self; - } - }; - - /* - Filter Chain - */ - var FilterChain = function(cb, req, res) { - var self = this; - - self.idx = 0; - self.cb = cb; - - self.req = req; - self.res = res; - }; - - FilterChain.prototype = { - next: function() { - var self = this, - req = self.req, - res = self.res; - - var mapper = Filter.filters[self.idx++]; - - //filter is complete, execute callback; - if (!mapper) return self.cb && self.cb(); - - /* - If not Matched go to next filter - If matched need to execute the req.next() in callback handler, - e.g: - webSvr.filter(/expression/, function(req, res) { - //filter actions - req.next(req, res); - }, options); - */ - if (mapper.match(req)) { - Logger.debug("filter matched", self.idx, mapper.expression, req.url); - - //filter matched, parse the request and then execute it - Parser(req, res, mapper); - }else{ - //filter not matched, validate next filter - self.next(); - } - } - }; - - /* - Http Handler: Execute and returned when when first matched; - At the same time only one Handler will be called; - */ - var Handler = { - handlers: [] - /* - url: add a new handler - expression: string/regexp [required] - handler: [many types] [required] - options: object [optional] - */ - , url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - Handler.handlers.push(mapper); - - return self; - } - - //Post: Parse the post data by default; - , post: function(expression, handler, options) { - return this.url(expression, handler, _.extend({ parse: true }, options)); - } - - //Session: Parse the session and post by default; - , session: function(expression, handler) { - return this.url(expression, handler, { parse: true, session: true }); - } - - , handle: function(req, res) { - //flag: is matched? - for(var i = 0, len = Handler.handlers.length; i < len ; i++) { - - var mapper = Handler.handlers[i]; - if (mapper.match(req)) { - - Logger.debug("handler matched", i, mapper.expression, req.url); - - var handler = mapper.handler, - type = handler.constructor.name; - - switch(type) { - //function: treated it as custom function handler - case "Function": - Parser(req, res, mapper); - break; - - //string: treated it as content - case "String": - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(handler); - break; - - //array: treated it as a file. - case "Array": - res.writeFile(handler[0]); - break; - } - return true; - } - } - - return false; - - } //end of handle - }; - - /* - ListDir: List all the files in a directory - */ - var ListDir = (function() { - - var urlFormat = function(url) { - url = url.replace(/\\/g,'/'); - url = url.replace(/ /g,'%20'); - return url; - }; - - //Align to right - var date = function(date) { - var d = date.getFullYear() - + '-' + (date.getMonth() + 1) - + '-' + (date.getDay() + 1) - + " " + date.toLocaleTimeString(); - return " ".substring(0, 20 - d.length) + d; - }; - - //Align to left - var size = function(num) { - return num + " ".substring(0, 12 - String(num).length); - }; - - //Create an anchor - var anchor = function(txt, url) { - url = url ? url : "/"; - return '' + txt + ""; - }; - - var listDir = { - //List all the files in a directory - list: function(req, res, dir) { - var url = req.url, - cur = 0, - len = 0; - - var listBegin = function() { - res.writeHead(200, {"Content-Type": "text/html"}); - res.write("

    http://" + req.headers.host + url + "


    "); - res.write("
    ");
    -          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    -        };
    -
    -        var listEnd = function() {
    -          res.write("

    "); - res.end("
    Count: " + len + "
    "); - }; - - listBegin(); - - fs.readdir(dir, function(err, files) { - if (err) { - listEnd(); - Logger.debug(err); - return; - } - - len = files.length; - - for(var idx = 0; idx < len; idx++) { - //Persistent the idx before make the sync process - (function(idx) { - var filePath = path.join(dir, files[idx]), - fileUrl = urlFormat(path.join(url, files[idx])); - - fs.stat(filePath, function(err, stat) { - cur++; - - if (err) { - Logger.debug(err); - }else{ - res.write( - date(stat.mtime) - + "\t" + size(stat.size) - + anchor(files[idx], fileUrl) - + "\r\n" - ); - } - - (cur == len) && listEnd(); - }); - })(idx); - } - - (len == 0) && listEnd(); - }); - } - }; - - return listDir; - }()); - - /* - * Templates - */ - var Template = (function() { - - var engine = require("./lib/doT"); - - //get a file - var getFile = function(filename, cb){ - var fullpath = path.join(Settings.root, filename); - - fs.readFile(fullpath, function (err, html) { - err && Logger.debug(err); - err ? cb("") : cb(html); - }); - }; - - //render a file - var render = function(chrunk, params, outFn){ - try { - tmplFn = engine.compile(chrunk, params); - outFn(tmplFn(params)); - } catch(err) { - Logger.debug(err); - outFn(err); - } - }; - - return { - //render templates - render: function(chrunk, params) { - var res = this, - end = res.end; - - var url = chrunk.url, - con = chrunk.constructor; - - //It's a http request (it has "url") - if (url) { - getFile(url, function(tmpl) { - render(tmpl, params, end); - }); - - //It's html contents (template codes) - } else if (con == String) { - render(chrunk, params, end); - - //It's Array object (template file path) - } else if (con == Array) { - getFile(chrunk[0], function(tmpl) { - render(tmpl, params, end); - }); - - //Nothing matched end the response - } else { - end(); - } - - } - } - }()); - - - /*****************Web initial codes*************/ - //Parameters - var root; - - var fileHandler = function(req, res) { - - var url = req.url, - hasQuery = url.indexOf("?"); - - //fs.stat can't recognize the file name with querystring; - url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - - var fullPath = path.join(root, url); - - //Handle path - var handlePath = function(phyPath) { - fs.stat(phyPath, function(err, stat) { - - //Consider as file not found - if (err) return self.write404(res); - - //Is file? Open this file and send to client. - if (stat.isFile()) { - // "If-modified-since" not defined, mark it as 1970-01-01 0:0:0 - var cacheTime = new Date(req.headers["if-modified-since"] || 1); - - // The file is modified - if (Settings.cache && stat.mtime <= cacheTime) { - res.writeHead(304); - res.end(); - - // Else send "not modifed" - } else { - res.setHeader("Last-Modified", stat.mtime.toUTCString()); - writeFile(res, phyPath); - } - } - - //Is Directory? - else if (stat.isDirectory()) { - handleDefault(phyPath); - } - - //Or write the 404 pages - else { - self.write404(res); - } - - }); - }; - - //List all the files and folders. - var handleDir = function(dirPath) { - Settings.listDir - ? ListDir.list(req, res, dirPath) - : self.write403(res); - }; - - //Handle default page - var handleDefault = function(dirPath) { - var defaultPage = Settings.defaultPage; - - if (defaultPage) { - var defaultPath = path.join(dirPath, defaultPage); - - fs.exists(defaultPath, function (exists) { - //If page exists hanle it again - if (exists) { - //In order to make it as a dir path for loading static resources - if (url[url.length - 1] != '/') { - return res.redirect(url + '/'); - } - - handlePath(defaultPath); - //If page doesn't exist hanlde the dir again - } else { - handleDir(dirPath); - } - }); - } else { - handleDir(dirPath); - } - }; - - handlePath(fullPath); - }; - - var requestHandler = function(req, res) { - //Response may be shutdown when do the filter, in order not to cause exception, - //Rewrite the write/writeHead functionalities of current response object - var endFn = res.end; - res.end = function() { - //Execute old end - endFn.apply(res, arguments); - //Rewirte write/writeHead on response object - res.write = res.writeHead = res.setHeader = function() { - Logger.debug("response is already end, response.write ignored!") - }; - }; - - //relative path, relative to the web root - res.writeFile = function(filePath, cb) { - self.writeFile(res, filePath, cb); - }; - - //absolute path, relative to the server running - res.sendFile = function(filePath, cb) { - self.writeFile(res, filePath, cb, true) - }; - - //301/302 : move permanently - res.redirect = function(url, status) { - res.writeHead(status ? status : 302, { "Location": url }); - res.end(); - }; - - //render template objects - res.render = Template.render; - - //initial httprequest - var filterChain = new FilterChain(function(){ - - //if handler not match, send the request - !Handler.handle(req, res) && fileHandler(req, res); - - }, req, res); - - //Hook FilterChain object on the request - req.filter = filterChain; - - //Handle the first filter - req.filter.next(); - }; - - var writeFile = function(res, fullPath) { - fs.readFile(fullPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - } - res.setHeader("Content-Type", mime.lookup(fullPath)); - res.writeHead(200); - res.end(data, "binary"); - }); - }; - - //API have function chain - //Filter - self.filter = Filter.filter; - self.file = Filter.file; - - //Handler - self.url = Handler.url; - self.post = Handler.post; - self.session = Handler.session; - - //Get a full path of a request - self.getFullPath = function(filePath) { - return path.join(root, filePath); - }; - - //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb, isSvrPath) { - !isSvrPath && (filePath = path.join(root, filePath)); - fs.exists(filePath, function(exist) { - if (exist) { - writeFile(res, filePath); - cb && cb(exist); - }else{ - //If callback function doesn't exist, write 404 page; - cb ? cb(exist) : self.write404(res); - } - }); - - return self; - }; - - self.write403 = function(res) { - res.writeHead(403, {"Content-Type": "text/html"}); - res.end("Access forbidden!"); - - return self; - }; - - self.write404 = function(res) { - res.writeHead(404, {"Content-Type": "text/html"}); - res.end("File not found!"); - - return self; - }; - - //start http server - self.start = function() { - //Update the default value of Settings - _.extend(Settings, options); - - root = Settings.root; - - //Create http server: Enable by default - if (Settings.port) { - var port = Settings.port; - - var httpSvr = http.createServer(requestHandler); - httpSvr.listen(port); - - Logger.log("Http server running at" - ,"Root:", root - ,"Port:", port - ); - - self.httpSvr = httpSvr; - } - - //Create https server: Disable by default - if ( options.httpsPort - && options.httpsKey - && options.httpsCert) { - - var httpsPort = Settings.httpsPort; - - var httpsSvr = https.createServer({ - key: Settings.httpsKey, - cert: Settings.httpsCert - }, requestHandler).listen(httpsPort); - - Logger.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort - ); - - self.httpsSvr = httpsSvr; - } - - /* - init modules - */ - //Start session garbage collection - SessionManager.start(); - - return self; - }; - - //stop http server - self.stop = function() { - self.httpSvr && self.httpSvr.close(); - self.httpsSvr && self.httpsSvr.close(); - SessionManager.stop(); - - return self; - }; - - //property: filters & handlers - Object.defineProperty(self, 'filters', { - get: function() { - return Filter.filters - }, - set: function(filters) { - Filter.filters = filters; - } - }); - - Object.defineProperty(self, 'handlers', { - get: function() { - return Handler.handlers; - }, - set: function(handlers) { - Handler.handlers = handlers; - } - }) - - return self; - -}; +/* +* Description: node-websvr +* Author: Kris Zhang +* Licenses: MIT +* Project url: https://github.com/newghost/node-websvr +* +* Referenced projects: +* Formidable: Support uploading files, integrate + https://github.com/felixge/node-formidable/ +* MIME: content-type in header + https://github.com/broofa/node-mime +* template: Template Engine + https://github.com/olado/doT +*/ + +//Node libraries +var fs = require("fs"); +var path = require("path"); +var qs = require("querystring"); +var os = require("os"); + +var http = require("http"); +var https = require("https"); + +//Open source libraries, some device may not have npm, so reference directly. +var mime = require("./lib/mime"); +var formidable = require('./lib/incoming_form'); + +/* +* Utility +*/ +var _ = { + //extend object to target + extend: function(tar, obj) { + if (!obj) return; + + for (var key in obj) { + tar[key] = obj[key]; + } + + return tar; + } +}; + +//Shortcuts +var define = Object.defineProperty; + + +/* +* Define and Export WebSvr +*/ +var WebSvr = module.exports = function(options) { + + var self = {}; + + /*****************Web module definitions*************/ + /* + Configurations + */ + var Settings = { + //root folder of web + root: "../" + + //http start + //default port of http + , port: 8054 + + //default port of https + , httpsPort: 8443 + , httpsKey: "" + , httpsCert: "" + + //list files in directory + , listDir: false + //enable client-side cache(304) + , cache: true + //enable debug information output + , debug: true + //receive buffer, default size 32k, etc: receive post data from ajax request + , bufferSize: 32768 + + //default pages, only one is supported + , defaultPage: "index.html" + + //logger file path + , logger: os.tmpDir() + "/log.txt" + + /* + Session timeout, in milliseconds. + When session is expired, session file will not deleted. + */ + , sessionTimeout: 1440000 + /* + Session garbage collection time, in milliseconds. + When session expired time is more than (sessionAge + sessionGCT), + then session file will be unlinked. + */ + , sessionGarbage: 3460000 + + //session file stored here + , sessionDir: os.tmpDir() + + //tempary upload file stored here + , uploadDir: os.tmpDir() + }; + + /* + Logger: log sth + */ + var Logger = (function() { + + var lineSeparator = "\r\n", + indentSeparator = "\t", + depth = 9; + + var write = function(logObj, dep) { + var depth = dep || depth, + output = new Date() + lineSeparator; + + function print(pre, obj) { + if (!obj) return; + for (var key in obj) { + var val = obj[key]; + output = output + pre + key + " : " + val + lineSeparator; + if (typeof val == "object") { + (pre.length < depth) && print(pre + indentSeparator, val); + } + } + } + + print(indentSeparator, logObj); + + fs.appendFile(Settings.logger, output, function(err) { + log(err); + }); + }; + + /* + Currnetly it's equal to console.log + */ + var log = function() { + console.log.apply(console, arguments); + }; + + /* + Add data before log information + */ + var debug = function() { + //diable console.log information + if (!Settings.debug) { + return; + } + + var d = new Date().toString(); + + Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + console.log.apply(console, arguments); + }; + + return { + log: log + , write: write + , debug: debug + }; + })(); + + /* + Body parser, parse the data in request body via + when parse complete, execute the callback, with response data; + */ + var BodyParser = function(req, res, callback) { + + var buffer = new Buffer(Settings.bufferSize); + + var length = 0, data = ""; + + req.on('data', function(chunk) { + chunk.copy(buffer, length, 0, chunk.length); + length += chunk.length; + }); + + req.on('end', function() { + data = length > 0 ? buffer.toString('utf8', 0, length) : ""; + callback(data); + }); + }; + + /* + Parse request with session support + */ + var SessionParser = function(req, res) { + var self = this; + + //session id + self.sid = null; + //session stored object + self.obj = null; + //is this new session? + self.new = false; + + //init session object + self.init(req, res); + }; + + SessionParser.prototype = { + init: function(req, res) { + var self = this, + sidKey = "_wsid", + sidVal; + + //Get or Create sid, sid exist in the cookie, read it + var cookie = req.headers.cookie || ""; + var idx = cookie.indexOf(sidKey + "="); + (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); + + //Sid doesn't exist, create it + if (idx < 0 || sidVal.length != 25) { + sidVal = SessionManager.create(); + res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + self.new = true; + }; + self.sid = sidVal; + + SessionManager.refresh(self.sid); + } + + //Create new session object + , newObj: function(key, cb) { + //Key is offered, return null of this key, else return empty session object + var self = this, + val = key ? null : {}; + + self.obj = {}; + cb && cb(val); + return val; + } + + //Get value from session object + , getVal: function(key, cb) { + var self = this; + + //key is null, return all the session object + var val = key ? self.obj[key] : self.obj; + cb && cb(val); + + return val; + } + + //Set an key/value pair in session object + , set: function(key, val, cb) { + var self = this; + + //Get session object first + self.get(function() { + + //Add or update key/value in session object + self.obj[key] = val; + + //Write or modify json file + fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { + if (err) { + Logger.debug(err); + return; + } + + cb && cb(self.obj); + + //force update + SessionManager.update(self.sid); + }); + }); + } + + //Get value from session file + , get: function(key, cb) { + var self = this; + + //The first parameter is callback function + if (key.constructor == Function) { + cb = key; + key = null; + } + + //The session object is already loaded + if (self.obj) return self.getVal(key, cb); + + //It's a new session file, need not to load it from file + if (self.new) return self.newObj(key, cb); + + var sessionPath = SessionManager.getPath(self.sid); + + //File operates, will cause delay + fs.exists(sessionPath, function(exists) { + //err: file doesn't exist + if (!exists) { + return self.newObj(key, cb); + + //session not expired + } else if (SessionManager.isValid(self.sid)) { + fs.readFile(sessionPath, function(err, data) { + if (err) { + Logger.debug(err); + return; + }; + data = data || "{}"; + self.obj = JSON.parse(data); + + return self.getVal(key, cb); + }); + + //session expired, treat it as new session + } else { + return self.newObj(key, cb); + } + }); + } + }; + + /* + Parser: Functions that Filter and Handler will be called + */ + var Parser = function(req, res, mapper) { + + var handler = mapper.handler; + + //add sesion support + var parseSession = function() { + //add sesion support + if (mapper.session && typeof req.session == "undefined") { + req.session = new SessionParser(req, res); + } + handler(req, res); + }; + + /* + parse data in request, this should be done before parse session, + because session stored in file + */ + var parseBody = function() { + //need to parse the request? + if (mapper.parse && typeof req.body == "undefined") { + //Must parser the request first, or the post data will lost; + BodyParser(req, res, function(data) { + req.body = data; + parseSession(); + }); + }else{ + parseSession(); + } + }; + + /* + parse file in request, this should be at the top of the list + */ + var parseFile = function() { + //Need to parse the file in request? + if (mapper.file && typeof req.body == "undefined") { + //Must parser the request first, or the post data maybe lost; + var form = new formidable.IncomingForm(); + + form.uploadDir = Settings.uploadDir; + + form.parse(req, function(err, fields, files) { + if (err) { + Logger.debug(err); + return; + }; + + //attach the parameters and files + req.body = fields; + req.files = files; + + //in fact request will not be parsed again, because body is not undefined + parseBody(); + }); + }else{ + parseBody(); + }; + }; + + parseFile(); + }; + + /* + SessionManager: + - Clear expired session files + - Valid session + */ + var SessionManager = (function() { + + //duration time + var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; + + //timer + var timer; + + //session array object, stored with {sid: [update time]}; + var list = {}; + + var getPath = function(sid) { + return path.join(Settings.sessionDir, sid); + }; + + //create a new session id + var create = function() { + //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions + var uuid + = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars + + '-' + + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars + + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars + + //fix the length to 25 + uuid += '00000000000000000000'.substr(0, 25 - uuid.length); + + return uuid; + }; + + //force update session in list + var update = function(sid, datetime) { + list[sid] = datetime || new Date(); + }; + + //remove a sesson from list + var remove = function(sid) { + //delete the file + fs.unlink(getPath(sid)); + //remove from list + delete list[sid]; + + Logger.debug("session removed", sid); + }; + + /* + Does session expired? + If the session is not in the list, add to the list. + i.e. When WebSvr restarted, session will not expired. + */ + var isValid = function(sid) { + var now = new Date(); + + !list[sid] && (list[sid] = now); + + return now - list[sid] <= Settings.sessionTimeout + }; + + /* + Session clean handler + */ + var cleanHandler = function() { + for (var sid in list) { + !isValid(sid) && remove(sid); + } + }; + + /* + Clean the session in temp folder + */ + var clean = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return Logger.debug(err); + + //converted to minutes + var expire = (+new Date() - gcTime) / 60000 | 0; + + files.forEach(function(file) { + if (file.length == 25) { + var stamp = parseInt(file.substr(0, file.indexOf('-'))); + + if (stamp) { + //remove the expired session + stamp < expire + ? remove(file) + : Logger.debug("session skipped", file); + } + } + }); + }); + }; + + //refresh session in list, valid first, if not expired, update the time + var refresh = function(sid, datetime) { + isValid(sid) && update(sid, datetime); + }; + + var stop = function() { + clearInterval(timer); + timer = null; + }; + + //stop before new session start + var start = function() { + //stop cleanHandler if available + stop(); + //clean the old sessions + clean(); + timer = setInterval(cleanHandler, gcTime); + }; + + return { + list: list, + create: create, + update: update, + remove: remove, + refresh: refresh, + isValid: isValid, + getPath: getPath, + clean: clean, + start: start, + stop: stop + } + })(); + + /* + Mapper: Used for Filter & Handler, + expression: required parameter + handler: required parameter + options: optional parameters + */ + var Mapper = function(expression, handler, options) { + var self = this; + + self.expression = expression; + self.handler = handler; + + //Has other parameters? + self.extend(options); + }; + + Mapper.prototype = { + /* + Does this mapper matched this request? + */ + match: function(req) { + var self = this, + expression = self.expression; + + //No expression? It's a general filter mapper + if (!expression) return true; + + switch (expression.constructor) { + case String: return req.url.indexOf(expression) > -1; + case RegExp: return expression.test(req.url); + } + + return false; + }, + + /* + Add optional parameters on current mapper + i.e: + session: boolean + file: boolean + parse: boolean + */ + extend: function(options) { + for(key in options) { + this[key] = options[key] + } + } + }; + + /* + Http Filter: Execute all the rules that matched, + Filter will be always called before a handler. + */ + var Filter = { + //filter list + filters: [] + + /* + filter: add a new filter + expression: string/regexp [optional] + handler: function [required] + options: object [optional] + */ + , filter: function(expression, handler, options) { + //The first parameter is Function => (handler, options) + if (expression.constructor == Function) { + options = handler; + handler = expression; + expression = null; + } + + var mapper = new Mapper(expression, handler, options); + Filter.filters.push(mapper); + + return self; + } + + /* + file receiver: it's a specfic filter, + this filter should be always at the top of the filter list + */ + , file: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, {file: true}); + //insert at the top of the filter array + Filter.filters.splice(0, 0, mapper); + + return self; + } + }; + + /* + Filter Chain + */ + var FilterChain = function(cb, req, res) { + var self = this; + + self.idx = 0; + self.cb = cb; + + self.req = req; + self.res = res; + }; + + FilterChain.prototype = { + next: function() { + var self = this, + req = self.req, + res = self.res; + + var mapper = Filter.filters[self.idx++]; + + //filter is complete, execute callback; + if (!mapper) return self.cb && self.cb(); + + /* + If not Matched go to next filter + If matched need to execute the req.next() in callback handler, + e.g: + webSvr.filter(/expression/, function(req, res) { + //filter actions + req.next(req, res); + }, options); + */ + if (mapper.match(req)) { + Logger.debug("filter matched", self.idx, mapper.expression, req.url); + + //filter matched, parse the request and then execute it + Parser(req, res, mapper); + }else{ + //filter not matched, validate next filter + self.next(); + } + } + }; + + /* + Http Handler: Execute and returned when when first matched; + At the same time only one Handler will be called; + */ + var Handler = { + handlers: [] + /* + url: add a new handler + expression: string/regexp [required] + handler: [many types] [required] + options: object [optional] + */ + , url: function(expression, handler, options) { + var mapper = new Mapper(expression, handler, options); + Handler.handlers.push(mapper); + + return self; + } + + //Post: Parse the post data by default; + , post: function(expression, handler, options) { + return this.url(expression, handler, _.extend({ parse: true }, options)); + } + + //Session: Parse the session and post by default; + , session: function(expression, handler) { + return this.url(expression, handler, { parse: true, session: true }); + } + + , handle: function(req, res) { + //flag: is matched? + for(var i = 0, len = Handler.handlers.length; i < len ; i++) { + + var mapper = Handler.handlers[i]; + if (mapper.match(req)) { + + Logger.debug("handler matched", i, mapper.expression, req.url); + + var handler = mapper.handler, + type = handler.constructor.name; + + switch(type) { + //function: treated it as custom function handler + case "Function": + Parser(req, res, mapper); + break; + + //string: treated it as content + case "String": + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(handler); + break; + + //array: treated it as a file. + case "Array": + res.writeFile(handler[0]); + break; + } + return true; + } + } + + return false; + + } //end of handle + }; + + /* + ListDir: List all the files in a directory + */ + var ListDir = (function() { + + var urlFormat = function(url) { + url = url.replace(/\\/g,'/'); + url = url.replace(/ /g,'%20'); + return url; + }; + + //Align to right + var date = function(date) { + var d = date.getFullYear() + + '-' + (date.getMonth() + 1) + + '-' + (date.getDay() + 1) + + " " + date.toLocaleTimeString(); + return " ".substring(0, 20 - d.length) + d; + }; + + //Align to left + var size = function(num) { + return num + " ".substring(0, 12 - String(num).length); + }; + + //Create an anchor + var anchor = function(txt, url) { + url = url ? url : "/"; + return '' + txt + ""; + }; + + var listDir = { + //List all the files in a directory + list: function(req, res, dir) { + var url = req.url, + cur = 0, + len = 0; + + var listBegin = function() { + res.writeHead(200, {"Content-Type": "text/html"}); + res.write("

    http://" + req.headers.host + url + "


    "); + res.write("
    ");
    +          res.write(anchor("[To Parent Directory]", url.substr(0, url.lastIndexOf('/'))) + "\r\n\r\n");
    +        };
    +
    +        var listEnd = function() {
    +          res.write("

    "); + res.end("
    Count: " + len + "
    "); + }; + + listBegin(); + + fs.readdir(dir, function(err, files) { + if (err) { + listEnd(); + Logger.debug(err); + return; + } + + len = files.length; + + for(var idx = 0; idx < len; idx++) { + //Persistent the idx before make the sync process + (function(idx) { + var filePath = path.join(dir, files[idx]), + fileUrl = urlFormat(path.join(url, files[idx])); + + fs.stat(filePath, function(err, stat) { + cur++; + + if (err) { + Logger.debug(err); + }else{ + res.write( + date(stat.mtime) + + "\t" + size(stat.size) + + anchor(files[idx], fileUrl) + + "\r\n" + ); + } + + (cur == len) && listEnd(); + }); + })(idx); + } + + (len == 0) && listEnd(); + }); + } + }; + + return listDir; + }()); + + /* + * Templates + */ + var Template = (function() { + + var engine = require("./lib/doT"); + + //get a file + var getFile = function(filename, cb){ + var fullpath = path.join(Settings.root, filename); + + fs.readFile(fullpath, function (err, html) { + err && Logger.debug(err); + err ? cb("") : cb(html); + }); + }; + + //render a file + var render = function(chrunk, params, outFn){ + try { + tmplFn = engine.compile(chrunk, params); + outFn(tmplFn(params)); + } catch(err) { + Logger.debug(err); + outFn(err); + } + }; + + return { + //render templates + render: function(chrunk, params) { + var res = this, + end = res.end; + + var url = chrunk.url, + con = chrunk.constructor; + + //It's a http request (it has "url") + if (url) { + getFile(url, function(tmpl) { + render(tmpl, params, end); + }); + + //It's html contents (template codes) + } else if (con == String) { + render(chrunk, params, end); + + //It's Array object (template file path) + } else if (con == Array) { + getFile(chrunk[0], function(tmpl) { + render(tmpl, params, end); + }); + + //Nothing matched end the response + } else { + end(); + } + + } + } + }()); + + + /*****************Web initial codes*************/ + //Parameters + var root; + + var fileHandler = function(req, res) { + + var url = req.url, + hasQuery = url.indexOf("?"); + + //fs.stat can't recognize the file name with querystring; + url = hasQuery > 0 ? url.substring(0, hasQuery) : url; + + var fullPath = path.join(root, url); + + //Handle path + var handlePath = function(phyPath) { + fs.stat(phyPath, function(err, stat) { + + //Consider as file not found + if (err) return self.write404(res); + + //Is file? Open this file and send to client. + if (stat.isFile()) { + // "If-modified-since" undefined, mark it as 1970-01-01 0:0:0 + var cacheTime = new Date(req.headers["if-modified-since"] || 1); + + // The file is modified + if (Settings.cache && stat.mtime <= cacheTime) { + res.writeHead(304); + res.end(); + + // Else send "not modifed" + } else { + res.setHeader("Last-Modified", stat.mtime.toUTCString()); + writeFile(res, phyPath); + } + } + + //Is Directory? + else if (stat.isDirectory()) { + handleDefault(phyPath); + } + + //Or write the 404 pages + else { + self.write404(res); + } + + }); + }; + + //List all the files and folders. + var handleDir = function(dirPath) { + Settings.listDir + ? ListDir.list(req, res, dirPath) + : self.write403(res); + }; + + //Handle default page + var handleDefault = function(dirPath) { + var defaultPage = Settings.defaultPage; + + if (defaultPage) { + var defaultPath = path.join(dirPath, defaultPage); + + fs.exists(defaultPath, function (exists) { + //If page exists hanle it again + if (exists) { + //In order to make it as a dir path for loading static resources + if (url[url.length - 1] != '/') { + return res.redirect(url + '/'); + } + + handlePath(defaultPath); + //If page doesn't exist hanlde the dir again + } else { + handleDir(dirPath); + } + }); + } else { + handleDir(dirPath); + } + }; + + handlePath(fullPath); + }; + + var requestHandler = function(req, res) { + //Response may be shutdown when do the filter, in order not to cause exception, + //Rewrite the write/writeHead functionalities of current response object + var endFn = res.end; + res.end = function() { + //Execute old end + endFn.apply(res, arguments); + //Rewirte write/writeHead on response object + res.write = res.writeHead = res.setHeader = function() { + Logger.debug("response is already end, response.write ignored!") + }; + }; + + //relative path, relative to the web root + res.writeFile = function(filePath, cb) { + self.writeFile(res, filePath, cb); + }; + + //absolute path, relative to the server running + res.sendFile = function(filePath, cb) { + self.writeFile(res, filePath, cb, true) + }; + + //301/302 : move permanently + res.redirect = function(url, status) { + res.writeHead(status ? status : 302, { "Location": url }); + res.end(); + }; + + //render template objects + res.render = Template.render; + + //initial httprequest + var filterChain = new FilterChain(function(){ + + //if handler not match, send the request + !Handler.handle(req, res) && fileHandler(req, res); + + }, req, res); + + //Hook FilterChain object on the request + req.filter = filterChain; + + //Handle the first filter + req.filter.next(); + }; + + var writeFile = function(res, fullPath) { + fs.readFile(fullPath, function(err, data) { + if (err) { + Logger.debug(err); + return; + } + res.setHeader("Content-Type", mime.lookup(fullPath)); + res.writeHead(200); + res.end(data, "binary"); + }); + }; + + //API have function chain + //Filter + self.filter = Filter.filter; + self.file = Filter.file; + + //Handler + self.url = Handler.url; + self.post = Handler.post; + self.session = Handler.session; + + //Get a full path of a request + self.getFullPath = function(filePath) { + return path.join(root, filePath); + }; + + //Write file, filePath is relative path + self.writeFile = function(res, filePath, cb, isSvrPath) { + !isSvrPath && (filePath = path.join(root, filePath)); + fs.exists(filePath, function(exist) { + if (exist) { + writeFile(res, filePath); + cb && cb(exist); + }else{ + //If callback function doesn't exist, write 404 page; + cb ? cb(exist) : self.write404(res); + } + }); + + return self; + }; + + self.write403 = function(res) { + res.writeHead(403, {"Content-Type": "text/html"}); + res.end("Access forbidden!"); + + return self; + }; + + self.write404 = function(res) { + res.writeHead(404, {"Content-Type": "text/html"}); + res.end("File not found!"); + + return self; + }; + + //start http server + self.start = function() { + //Update the default value of Settings + _.extend(Settings, options); + + root = Settings.root; + + //Create http server: Enable by default + if (Settings.port) { + var port = Settings.port; + + var httpSvr = http.createServer(requestHandler); + httpSvr.listen(port); + + Logger.log("Http server running at" + ,"Root:", root + ,"Port:", port + ); + + self.httpSvr = httpSvr; + } + + //Create https server: Disable by default + if ( options.httpsPort + && options.httpsKey + && options.httpsCert) { + + var httpsPort = Settings.httpsPort; + + var httpsSvr = https.createServer({ + key: Settings.httpsKey, + cert: Settings.httpsCert + }, requestHandler).listen(httpsPort); + + Logger.log("Https server running at" + ,"Root:", root + ,"Port:", httpsPort + ); + + self.httpsSvr = httpsSvr; + } + + /* + init modules + */ + //Start session garbage collection + SessionManager.start(); + + return self; + }; + + //stop http server + self.stop = function() { + self.httpSvr && self.httpSvr.close(); + self.httpsSvr && self.httpsSvr.close(); + SessionManager.stop(); + + return self; + }; + + //property: filters & handlers + define(self, 'filters', { + get: function() { + return Filter.filters + }, + set: function(filters) { + Filter.filters = filters; + } + }); + + define(self, 'handlers', { + get: function() { + return Handler.handlers; + }, + set: function(handlers) { + Handler.handlers = handlers; + } + }); + + return self; + +}; From 66b518fd126d4fefa7e68c347d04dda253b05372 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 7 May 2013 18:00:59 +0800 Subject: [PATCH 069/195] version: 1.04, add package.json --- package.json | 20 ++++++++++++++++++++ website/start.sh | 1 + websvr/websvr.js | 6 +++--- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 package.json mode change 100644 => 100755 website/start.sh diff --git a/package.json b/package.json new file mode 100644 index 0000000..99a69e3 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "websvr", + "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", + "version": "0.1.0", + "author": "Kris Zhang ", + "dependencies": { + "dot": "1.0.0", + "formidable": "1.0.14", + "mime": "1.2.9" + }, + "keywords": [ + "web", + "server", + "https", + "filter", + "handler" + ], + "repository": "git://github.com/newghost/websvr.git", + "main": "./websvr/websvr.js" +} \ No newline at end of file diff --git a/website/start.sh b/website/start.sh old mode 100644 new mode 100755 index dca9911..3fe9516 --- a/website/start.sh +++ b/website/start.sh @@ -11,5 +11,6 @@ while true; do echo Server stop working, restarting... echo *************************************** + #wait 2 seconds sleep 2 done \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index e9323be..2cb921e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -23,8 +23,8 @@ var http = require("http"); var https = require("https"); //Open source libraries, some device may not have npm, so reference directly. -var mime = require("./lib/mime"); -var formidable = require('./lib/incoming_form'); +var mime = require("mime"); +var formidable = require("formidable"); /* * Utility @@ -812,7 +812,7 @@ var WebSvr = module.exports = function(options) { */ var Template = (function() { - var engine = require("./lib/doT"); + var engine = require("dot"); //get a file var getFile = function(filename, cb){ From ae3bb84826a2093409baf18791827638c94e2807 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 7 May 2013 18:14:52 +0800 Subject: [PATCH 070/195] delete unused packages. --- websvr/lib/doT.js | 131 --- websvr/lib/file.js | 72 -- websvr/lib/incoming_form.js | 531 ---------- websvr/lib/json_parser.js | 35 - websvr/lib/mime.js | 114 --- websvr/lib/multipart_parser.js | 324 ------ websvr/lib/octet_parser.js | 20 - websvr/lib/querystring_parser.js | 27 - websvr/lib/types/mime.types | 1588 ------------------------------ websvr/lib/types/node.types | 71 -- 10 files changed, 2913 deletions(-) delete mode 100644 websvr/lib/doT.js delete mode 100644 websvr/lib/file.js delete mode 100644 websvr/lib/incoming_form.js delete mode 100644 websvr/lib/json_parser.js delete mode 100644 websvr/lib/mime.js delete mode 100644 websvr/lib/multipart_parser.js delete mode 100644 websvr/lib/octet_parser.js delete mode 100644 websvr/lib/querystring_parser.js delete mode 100644 websvr/lib/types/mime.types delete mode 100644 websvr/lib/types/node.types diff --git a/websvr/lib/doT.js b/websvr/lib/doT.js deleted file mode 100644 index c44f245..0000000 --- a/websvr/lib/doT.js +++ /dev/null @@ -1,131 +0,0 @@ -// doT.js -// 2011, Laura Doktorova, https://github.com/olado/doT -// -// doT.js is an open source component of http://bebedo.com -// Licensed under the MIT license. -// -(function() { - "use strict"; - - var doT = { - version: '0.2.0', - templateSettings: { - evaluate: /\{\{([\s\S]+?)\}\}/g, - interpolate: /\{\{=([\s\S]+?)\}\}/g, - encode: /\{\{!([\s\S]+?)\}\}/g, - use: /\{\{#([\s\S]+?)\}\}/g, - define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g, - conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g, - iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g, - varname: 'it', - strip: true, - append: true, - selfcontained: false - }, - template: undefined, //fn, compile template - compile: undefined //fn, for express - }; - - var global = (function(){ return this || (0,eval)('this'); }()); - - if (typeof module !== 'undefined' && module.exports) { - module.exports = doT; - } else if (typeof define === 'function' && define.amd) { - define(function(){return doT;}); - } else { - global.doT = doT; - } - - function encodeHTMLSource() { - var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }, - matchHTML = /&(?!#?\w+;)|<|>|"|'|\//g; - return function(code) { - return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : code; - }; - } - global.encodeHTML = encodeHTMLSource(); - - var startend = { - append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" }, - split: { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML("} - }, skip = /$^/; - - function resolveDefs(c, block, def) { - return ((typeof block === 'string') ? block : block.toString()) - .replace(c.define || skip, function(m, code, assign, value) { - if (code.indexOf('def.') === 0) { - code = code.substring(4); - } - if (!(code in def)) { - if (assign === ':') { - def[code]= value; - } else { - eval("def['"+code+"']=" + value); - } - } - return ''; - }) - .replace(c.use || skip, function(m, code) { - var v = eval(code); - return v ? resolveDefs(c, v, def) : v; - }); - } - - function unescape(code) { - return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, ' '); - } - - doT.template = function(tmpl, c, def) { - c = c || doT.templateSettings; - var cse = c.append ? startend.append : startend.split, str, needhtmlencode, sid=0, indv; - - if (c.use || c.define) { - var olddef = global.def; global.def = def || {}; // workaround minifiers - str = resolveDefs(c, tmpl, global.def); - global.def = olddef; - } else str = tmpl; - - str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g,' ') - .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,''): str) - .replace(/'|\\/g, '\\$&') - .replace(c.interpolate || skip, function(m, code) { - return cse.start + unescape(code) + cse.end; - }) - .replace(c.encode || skip, function(m, code) { - needhtmlencode = true; - return cse.startencode + unescape(code) + cse.end; - }) - .replace(c.conditional || skip, function(m, elsecase, code) { - return elsecase ? - (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") : - (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='"); - }) - .replace(c.iterate || skip, function(m, iterate, vname, iname) { - if (!iterate) return "';} } out+='"; - sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate); - return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+" self.maxFieldsSize) { - self._error(new Error('maxFieldsSize exceeded, received '+self._fieldsSize+' bytes of field data')); - return; - } - value += decoder.write(buffer); - }); - - part.on('end', function() { - self.emit('field', part.name, value); - }); - return; - } - - this._flushing++; - - var file = new File({ - path: this._uploadPath(part.filename), - name: part.filename, - type: part.mime, - hash: self.hash - }); - - this.emit('fileBegin', part.name, file); - - file.open(); - this.openedFiles.push(file); - - part.on('data', function(buffer) { - self.pause(); - file.write(buffer, function() { - self.resume(); - }); - }); - - part.on('end', function() { - file.end(function() { - self._flushing--; - self.emit('file', part.name, file); - self._maybeEnd(); - }); - }); -}; - -function dummyParser(self) { - return { - end: function () { - self.ended = true; - self._maybeEnd(); - return null; - } - }; -} - -IncomingForm.prototype._parseContentType = function() { - if (this.bytesExpected === 0) { - this._parser = dummyParser(this); - return; - } - - if (!this.headers['content-type']) { - this._error(new Error('bad content-type header, no content-type')); - return; - } - - if (this.headers['content-type'].match(/octet-stream/i)) { - this._initOctetStream(); - return; - } - - if (this.headers['content-type'].match(/urlencoded/i)) { - this._initUrlencoded(); - return; - } - - if (this.headers['content-type'].match(/multipart/i)) { - var m; - if (m = this.headers['content-type'].match(/boundary=(?:"([^"]+)"|([^;]+))/i)) { - this._initMultipart(m[1] || m[2]); - } else { - this._error(new Error('bad content-type header, no multipart boundary')); - } - return; - } - - if (this.headers['content-type'].match(/json/i)) { - this._initJSONencoded(); - return; - } - - this._error(new Error('bad content-type header, unknown content-type: '+this.headers['content-type'])); -}; - -IncomingForm.prototype._error = function(err) { - if (this.error || this.ended) { - return; - } - - this.error = err; - this.pause(); - this.emit('error', err); - - if (Array.isArray(this.openedFiles)) { - this.openedFiles.forEach(function(file) { - file._writeStream.destroy(); - setTimeout(fs.unlink, 0, file.path); - }); - } -}; - -IncomingForm.prototype._parseContentLength = function() { - this.bytesReceived = 0; - if (this.headers['content-length']) { - this.bytesExpected = parseInt(this.headers['content-length'], 10); - } else if (this.headers['transfer-encoding'] === undefined) { - this.bytesExpected = 0; - } - - if (this.bytesExpected !== null) { - this.emit('progress', this.bytesReceived, this.bytesExpected); - } -}; - -IncomingForm.prototype._newParser = function() { - return new MultipartParser(); -}; - -IncomingForm.prototype._initMultipart = function(boundary) { - this.type = 'multipart'; - - var parser = new MultipartParser(), - self = this, - headerField, - headerValue, - part; - - parser.initWithBoundary(boundary); - - parser.onPartBegin = function() { - part = new Stream(); - part.readable = true; - part.headers = {}; - part.name = null; - part.filename = null; - part.mime = null; - - part.transferEncoding = 'binary'; - part.transferBuffer = ''; - - headerField = ''; - headerValue = ''; - }; - - parser.onHeaderField = function(b, start, end) { - headerField += b.toString(self.encoding, start, end); - }; - - parser.onHeaderValue = function(b, start, end) { - headerValue += b.toString(self.encoding, start, end); - }; - - parser.onHeaderEnd = function() { - headerField = headerField.toLowerCase(); - part.headers[headerField] = headerValue; - - var m; - if (headerField == 'content-disposition') { - if (m = headerValue.match(/\bname="([^"]+)"/i)) { - part.name = m[1]; - } - - part.filename = self._fileName(headerValue); - } else if (headerField == 'content-type') { - part.mime = headerValue; - } else if (headerField == 'content-transfer-encoding') { - part.transferEncoding = headerValue.toLowerCase(); - } - - headerField = ''; - headerValue = ''; - }; - - parser.onHeadersEnd = function() { - switch(part.transferEncoding){ - case 'binary': - case '7bit': - case '8bit': - parser.onPartData = function(b, start, end) { - part.emit('data', b.slice(start, end)); - }; - - parser.onPartEnd = function() { - part.emit('end'); - }; - break; - - case 'base64': - parser.onPartData = function(b, start, end) { - part.transferBuffer += b.slice(start, end).toString('ascii'); - - /* - four bytes (chars) in base64 converts to three bytes in binary - encoding. So we should always work with a number of bytes that - can be divided by 4, it will result in a number of buytes that - can be divided vy 3. - */ - var offset = parseInt(part.transferBuffer.length / 4) * 4; - part.emit('data', new Buffer(part.transferBuffer.substring(0, offset), 'base64')) - part.transferBuffer = part.transferBuffer.substring(offset); - }; - - parser.onPartEnd = function() { - part.emit('data', new Buffer(part.transferBuffer, 'base64')) - part.emit('end'); - }; - break; - - default: - return self._error(new Error('unknown transfer-encoding')); - } - - self.onPart(part); - }; - - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._fileName = function(headerValue) { - var m = headerValue.match(/\bfilename="(.*?)"($|; )/i); - if (!m) return; - - var filename = m[1].substr(m[1].lastIndexOf('\\') + 1); - filename = filename.replace(/%22/g, '"'); - filename = filename.replace(/&#([\d]{4});/g, function(m, code) { - return String.fromCharCode(code); - }); - return filename; -}; - -IncomingForm.prototype._initUrlencoded = function() { - this.type = 'urlencoded'; - - var parser = new QuerystringParser(this.maxFields) - , self = this; - - parser.onField = function(key, val) { - self.emit('field', key, val); - }; - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._initOctetStream = function() { - this.type = 'octet-stream'; - var filename = this.headers['x-file-name']; - var mime = this.headers['content-type']; - - var file = new File({ - path: this._uploadPath(filename), - name: filename, - type: mime - }); - - file.open(); - - this.emit('fileBegin', filename, file); - - this._flushing++; - - var self = this; - - self._parser = new OctetParser(); - - //Keep track of writes that haven't finished so we don't emit the file before it's done being written - var outstandingWrites = 0; - - self._parser.on('data', function(buffer){ - self.pause(); - outstandingWrites++; - - file.write(buffer, function() { - outstandingWrites--; - self.resume(); - - if(self.ended){ - self._parser.emit('doneWritingFile'); - } - }); - }); - - self._parser.on('end', function(){ - self._flushing--; - self.ended = true; - - var done = function(){ - self.emit('file', 'file', file); - self._maybeEnd(); - }; - - if(outstandingWrites === 0){ - done(); - } else { - self._parser.once('doneWritingFile', done); - } - }); -}; - -IncomingForm.prototype._initJSONencoded = function() { - this.type = 'json'; - - var parser = new JSONParser() - , self = this; - - if (this.bytesExpected) { - parser.initWithLength(this.bytesExpected); - } - - parser.onField = function(key, val) { - self.emit('field', key, val); - } - - parser.onEnd = function() { - self.ended = true; - self._maybeEnd(); - }; - - this._parser = parser; -}; - -IncomingForm.prototype._uploadPath = function(filename) { - var name = ''; - for (var i = 0; i < 32; i++) { - name += Math.floor(Math.random() * 16).toString(16); - } - - if (this.keepExtensions) { - var ext = path.extname(filename); - ext = ext.replace(/(\.[a-z0-9]+).*/, '$1'); - - name += ext; - } - - return path.join(this.uploadDir, name); -}; - -IncomingForm.prototype._maybeEnd = function() { - if (!this.ended || this._flushing || this.error) { - return; - } - - this.emit('end'); -}; - diff --git a/websvr/lib/json_parser.js b/websvr/lib/json_parser.js deleted file mode 100644 index 6ce966b..0000000 --- a/websvr/lib/json_parser.js +++ /dev/null @@ -1,35 +0,0 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -var Buffer = require('buffer').Buffer - -function JSONParser() { - this.data = new Buffer(''); - this.bytesWritten = 0; -}; -exports.JSONParser = JSONParser; - -JSONParser.prototype.initWithLength = function(length) { - this.data = new Buffer(length); -} - -JSONParser.prototype.write = function(buffer) { - if (this.data.length >= this.bytesWritten + buffer.length) { - buffer.copy(this.data, this.bytesWritten); - } else { - this.data = Buffer.concat([this.data, buffer]); - } - this.bytesWritten += buffer.length; - return buffer.length; -} - -JSONParser.prototype.end = function() { - try { - var fields = JSON.parse(this.data.toString('utf8')) - for (var field in fields) { - this.onField(field, fields[field]); - } - } catch (e) {} - this.data = null; - - this.onEnd(); -} \ No newline at end of file diff --git a/websvr/lib/mime.js b/websvr/lib/mime.js deleted file mode 100644 index 8a7eb09..0000000 --- a/websvr/lib/mime.js +++ /dev/null @@ -1,114 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -function Mime() { - // Map of extension -> mime type - this.types = Object.create(null); - - // Map of mime type -> extension - this.extensions = Object.create(null); -} - -/** - * Define mimetype -> extension mappings. Each key is a mime-type that maps - * to an array of extensions associated with the type. The first extension is - * used as the default extension for the type. - * - * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']}); - * - * @param map (Object) type definitions - */ -Mime.prototype.define = function (map) { - for (var type in map) { - var exts = map[type]; - - for (var i = 0; i < exts.length; i++) { - if (process.env.DEBUG_MIME && this.types[exts]) { - console.warn(this._loading.replace(/.*\//, ''), 'changes "' + exts[i] + '" extension type from ' + - this.types[exts] + ' to ' + type); - } - - this.types[exts[i]] = type; - } - - // Default extension is the first one we encounter - if (!this.extensions[type]) { - this.extensions[type] = exts[0]; - } - } -}; - -/** - * Load an Apache2-style ".types" file - * - * This may be called multiple times (it's expected). Where files declare - * overlapping types/extensions, the last file wins. - * - * @param file (String) path of file to load. - */ -Mime.prototype.load = function(file) { - - this._loading = file; - // Read file and split into lines - var map = {}, - content = fs.readFileSync(file, 'ascii'), - lines = content.split(/[\r\n]+/); - - lines.forEach(function(line) { - // Clean up whitespace/comments, and split into fields - var fields = line.replace(/\s*#.*|^\s*|\s*$/g, '').split(/\s+/); - map[fields.shift()] = fields; - }); - - this.define(map); - - this._loading = null; -}; - -/** - * Lookup a mime type based on extension - */ -Mime.prototype.lookup = function(path, fallback) { - var ext = path.replace(/.*[\.\/]/, '').toLowerCase(); - - return this.types[ext] || fallback || this.default_type; -}; - -/** - * Return file extension associated with a mime type - */ -Mime.prototype.extension = function(mimeType) { - var type = mimeType.match(/^\s*([^;\s]*)(?:;|\s|$)/)[1].toLowerCase(); - return this.extensions[type]; -}; - -// Default instance -var mime = new Mime(); - -// Load local copy of -// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -mime.load(path.join(__dirname, 'types/mime.types')); - -// Load additional types from node.js community -mime.load(path.join(__dirname, 'types/node.types')); - -// Default type -mime.default_type = mime.lookup('bin'); - -// -// Additional API specific to the default instance -// - -mime.Mime = Mime; - -/** - * Lookup a charset based on mime type. - */ -mime.charsets = { - lookup: function(mimeType, fallback) { - // Assume text types are utf8 - return (/^text\//).test(mimeType) ? 'UTF-8' : fallback; - } -}; - -module.exports = mime; diff --git a/websvr/lib/multipart_parser.js b/websvr/lib/multipart_parser.js deleted file mode 100644 index 98a6856..0000000 --- a/websvr/lib/multipart_parser.js +++ /dev/null @@ -1,324 +0,0 @@ -var Buffer = require('buffer').Buffer, - s = 0, - S = - { PARSER_UNINITIALIZED: 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, - Z = 122, - - lower = function(c) { - return c | 0x20; - }; - -for (s in S) { - exports[s] = S[s]; -} - -function MultipartParser() { - this.boundary = null; - this.boundaryChars = null; - this.lookbehind = null; - this.state = S.PARSER_UNINITIALIZED; - - this.index = null; - this.flags = 0; -}; -exports.MultipartParser = MultipartParser; - -MultipartParser.stateToString = function(stateNumber) { - for (var state in S) { - var number = S[state]; - if (number === stateNumber) return state; - } -}; - -MultipartParser.prototype.initWithBoundary = function(str) { - this.boundary = new Buffer(str.length+4); - this.boundary.write('\r\n--', 'ascii', 0); - this.boundary.write(str, 'ascii', 4); - this.lookbehind = new Buffer(this.boundary.length+8); - this.state = S.START; - - this.boundaryChars = {}; - for (var i = 0; i < this.boundary.length; i++) { - this.boundaryChars[this.boundary[i]] = true; - } -}; - -MultipartParser.prototype.write = function(buffer) { - var self = this, - i = 0, - len = buffer.length, - prevIndex = this.index, - index = this.index, - state = this.state, - flags = this.flags, - lookbehind = this.lookbehind, - boundary = this.boundary, - boundaryChars = this.boundaryChars, - boundaryLength = this.boundary.length, - boundaryEnd = boundaryLength - 1, - bufferLength = buffer.length, - c, - cl, - - mark = function(name) { - self[name+'Mark'] = i; - }, - clear = function(name) { - delete self[name+'Mark']; - }, - callback = function(name, buffer, start, end) { - if (start !== undefined && start === end) { - return; - } - - var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); - if (callbackSymbol in self) { - self[callbackSymbol](buffer, start, end); - } - }, - dataCallback = function(name, clear) { - var markSymbol = name+'Mark'; - if (!(markSymbol in self)) { - return; - } - - if (!clear) { - callback(name, buffer, self[markSymbol], buffer.length); - self[markSymbol] = 0; - } else { - callback(name, buffer, self[markSymbol], i); - delete self[markSymbol]; - } - }; - - for (i = 0; i < len; i++) { - c = buffer[i]; - switch (state) { - case S.PARSER_UNINITIALIZED: - return i; - case S.START: - index = 0; - state = S.START_BOUNDARY; - case S.START_BOUNDARY: - if (index == boundary.length - 2) { - if (c != CR) { - return i; - } - index++; - break; - } else if (index - 1 == boundary.length - 2) { - if (c != LF) { - return i; - } - index = 0; - callback('partBegin'); - state = S.HEADER_FIELD_START; - break; - } - - if (c != boundary[index+2]) { - index = -2; - } - if (c == boundary[index+2]) { - index++; - } - break; - case S.HEADER_FIELD_START: - state = S.HEADER_FIELD; - mark('headerField'); - index = 0; - case S.HEADER_FIELD: - if (c == CR) { - clear('headerField'); - state = S.HEADERS_ALMOST_DONE; - break; - } - - index++; - if (c == HYPHEN) { - break; - } - - if (c == COLON) { - if (index == 1) { - // empty header field - return i; - } - dataCallback('headerField', true); - state = S.HEADER_VALUE_START; - break; - } - - cl = lower(c); - if (cl < A || cl > Z) { - return i; - } - break; - case S.HEADER_VALUE_START: - if (c == SPACE) { - break; - } - - mark('headerValue'); - state = S.HEADER_VALUE; - case S.HEADER_VALUE: - if (c == CR) { - dataCallback('headerValue', true); - 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; - } - - callback('headersEnd'); - state = S.PART_DATA_START; - break; - case S.PART_DATA_START: - state = S.PART_DATA; - mark('partData'); - 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 < boundary.length) { - if (boundary[index] == c) { - if (index == 0) { - dataCallback('partData', true); - } - index++; - } else { - index = 0; - } - } else if (index == boundary.length) { - 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 == boundary.length) { - if (flags & F.PART_BOUNDARY) { - index = 0; - if (c == LF) { - // unset the PART_BOUNDARY flag - flags &= ~F.PART_BOUNDARY; - callback('partEnd'); - callback('partBegin'); - state = S.HEADER_FIELD_START; - break; - } - } else if (flags & F.LAST_BOUNDARY) { - if (c == HYPHEN) { - callback('partEnd'); - 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 - callback('partData', lookbehind, 0, prevIndex); - prevIndex = 0; - mark('partData'); - - // 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; - } - } - - dataCallback('headerField'); - dataCallback('headerValue'); - dataCallback('partData'); - - this.index = index; - this.state = state; - this.flags = flags; - - return len; -}; - -MultipartParser.prototype.end = function() { - var callback = function(self, name) { - var callbackSymbol = 'on'+name.substr(0, 1).toUpperCase()+name.substr(1); - if (callbackSymbol in self) { - self[callbackSymbol](); - } - }; - if ((this.state == S.HEADER_FIELD_START && this.index == 0) || - (this.state == S.PART_DATA && this.index == this.boundary.length)) { - callback(this, 'partEnd'); - callback(this, 'end'); - } else if (this.state != S.END) { - return new Error('MultipartParser.end(): stream ended unexpectedly: ' + this.explain()); - } -}; - -MultipartParser.prototype.explain = function() { - return 'state = ' + MultipartParser.stateToString(this.state); -}; diff --git a/websvr/lib/octet_parser.js b/websvr/lib/octet_parser.js deleted file mode 100644 index 6e8b551..0000000 --- a/websvr/lib/octet_parser.js +++ /dev/null @@ -1,20 +0,0 @@ -var EventEmitter = require('events').EventEmitter - , util = require('util'); - -function OctetParser(options){ - if(!(this instanceof OctetParser)) return new OctetParser(options); - EventEmitter.call(this); -} - -util.inherits(OctetParser, EventEmitter); - -exports.OctetParser = OctetParser; - -OctetParser.prototype.write = function(buffer) { - this.emit('data', buffer); - return buffer.length; -}; - -OctetParser.prototype.end = function() { - this.emit('end'); -}; diff --git a/websvr/lib/querystring_parser.js b/websvr/lib/querystring_parser.js deleted file mode 100644 index 320ce5a..0000000 --- a/websvr/lib/querystring_parser.js +++ /dev/null @@ -1,27 +0,0 @@ -if (global.GENTLY) require = GENTLY.hijack(require); - -// This is a buffering parser, not quite as nice as the multipart one. -// If I find time I'll rewrite this to be fully streaming as well -var querystring = require('querystring'); - -function QuerystringParser(maxKeys) { - this.maxKeys = maxKeys; - this.buffer = ''; -}; -exports.QuerystringParser = QuerystringParser; - -QuerystringParser.prototype.write = function(buffer) { - this.buffer += buffer.toString('ascii'); - return buffer.length; -}; - -QuerystringParser.prototype.end = function() { - var fields = querystring.parse(this.buffer, '&', '=', { maxKeys: this.maxKeys }); - for (var field in fields) { - this.onField(field, fields[field]); - } - this.buffer = ''; - - this.onEnd(); -}; - diff --git a/websvr/lib/types/mime.types b/websvr/lib/types/mime.types deleted file mode 100644 index b90b165..0000000 --- a/websvr/lib/types/mime.types +++ /dev/null @@ -1,1588 +0,0 @@ -# This file maps Internet media types to unique file extension(s). -# Although created for httpd, this file is used by many software systems -# and has been placed in the public domain for unlimited redisribution. -# -# The table below contains both registered and (common) unregistered types. -# A type that has no unique extension can be ignored -- they are listed -# here to guide configurations toward known types and to make it easier to -# identify "new" types. File extensions are also commonly used to indicate -# content languages and encodings, so choose them carefully. -# -# Internet media types should be registered as described in RFC 4288. -# The registry is at . -# -# MIME type (lowercased) Extensions -# ============================================ ========== -# application/1d-interleaved-parityfec -# application/3gpp-ims+xml -# application/activemessage -application/andrew-inset ez -# application/applefile -application/applixware aw -application/atom+xml atom -application/atomcat+xml atomcat -# application/atomicmail -application/atomsvc+xml atomsvc -# application/auth-policy+xml -# application/batch-smtp -# application/beep+xml -# application/calendar+xml -# application/cals-1840 -# application/ccmp+xml -application/ccxml+xml ccxml -application/cdmi-capability cdmia -application/cdmi-container cdmic -application/cdmi-domain cdmid -application/cdmi-object cdmio -application/cdmi-queue cdmiq -# application/cea-2018+xml -# application/cellml+xml -# application/cfw -# application/cnrp+xml -# application/commonground -# application/conference-info+xml -# application/cpl+xml -# application/csta+xml -# application/cstadata+xml -application/cu-seeme cu -# application/cybercash -application/davmount+xml davmount -# application/dca-rft -# application/dec-dx -# application/dialog-info+xml -# application/dicom -# application/dns -application/docbook+xml dbk -# application/dskpp+xml -application/dssc+der dssc -application/dssc+xml xdssc -# application/dvcs -application/ecmascript ecma -# application/edi-consent -# application/edi-x12 -# application/edifact -application/emma+xml emma -# application/epp+xml -application/epub+zip epub -# application/eshop -# application/example -application/exi exi -# application/fastinfoset -# application/fastsoap -# application/fits -application/font-tdpfr pfr -# application/framework-attributes+xml -application/gml+xml gml -application/gpx+xml gpx -application/gxf gxf -# application/h224 -# application/held+xml -# application/http -application/hyperstudio stk -# application/ibe-key-request+xml -# application/ibe-pkg-reply+xml -# application/ibe-pp-data -# application/iges -# application/im-iscomposing+xml -# application/index -# application/index.cmd -# application/index.obj -# application/index.response -# application/index.vnd -application/inkml+xml ink inkml -# application/iotp -application/ipfix ipfix -# application/ipp -# application/isup -application/java-archive jar -application/java-serialized-object ser -application/java-vm class -application/javascript js -application/json json -application/jsonml+json jsonml -# application/kpml-request+xml -# application/kpml-response+xml -application/lost+xml lostxml -application/mac-binhex40 hqx -application/mac-compactpro cpt -# application/macwriteii -application/mads+xml mads -application/marc mrc -application/marcxml+xml mrcx -application/mathematica ma nb mb -# application/mathml-content+xml -# application/mathml-presentation+xml -application/mathml+xml mathml -# application/mbms-associated-procedure-description+xml -# application/mbms-deregister+xml -# application/mbms-envelope+xml -# application/mbms-msk+xml -# application/mbms-msk-response+xml -# application/mbms-protection-description+xml -# application/mbms-reception-report+xml -# application/mbms-register+xml -# application/mbms-register-response+xml -# application/mbms-user-service-description+xml -application/mbox mbox -# application/media_control+xml -application/mediaservercontrol+xml mscml -application/metalink+xml metalink -application/metalink4+xml meta4 -application/mets+xml mets -# application/mikey -application/mods+xml mods -# application/moss-keys -# application/moss-signature -# application/mosskey-data -# application/mosskey-request -application/mp21 m21 mp21 -application/mp4 mp4s -# application/mpeg4-generic -# application/mpeg4-iod -# application/mpeg4-iod-xmt -# application/msc-ivr+xml -# application/msc-mixer+xml -application/msword doc dot -application/mxf mxf -# application/nasdata -# application/news-checkgroups -# application/news-groupinfo -# application/news-transmission -# application/nss -# application/ocsp-request -# application/ocsp-response -application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy -application/oda oda -application/oebps-package+xml opf -application/ogg ogx -application/omdoc+xml omdoc -application/onenote onetoc onetoc2 onetmp onepkg -application/oxps oxps -# application/parityfec -application/patch-ops-error+xml xer -application/pdf pdf -application/pgp-encrypted pgp -# application/pgp-keys -application/pgp-signature asc sig -application/pics-rules prf -# application/pidf+xml -# application/pidf-diff+xml -application/pkcs10 p10 -application/pkcs7-mime p7m p7c -application/pkcs7-signature p7s -application/pkcs8 p8 -application/pkix-attr-cert ac -application/pkix-cert cer -application/pkix-crl crl -application/pkix-pkipath pkipath -application/pkixcmp pki -application/pls+xml pls -# application/poc-settings+xml -application/postscript ai eps ps -# application/prs.alvestrand.titrax-sheet -application/prs.cww cww -# application/prs.nprend -# application/prs.plucker -# application/prs.rdf-xml-crypt -# application/prs.xsf+xml -application/pskc+xml pskcxml -# application/qsig -application/rdf+xml rdf -application/reginfo+xml rif -application/relax-ng-compact-syntax rnc -# application/remote-printing -application/resource-lists+xml rl -application/resource-lists-diff+xml rld -# application/riscos -# application/rlmi+xml -application/rls-services+xml rs -application/rpki-ghostbusters gbr -application/rpki-manifest mft -application/rpki-roa roa -# application/rpki-updown -application/rsd+xml rsd -application/rss+xml rss -application/rtf rtf -# application/rtx -# application/samlassertion+xml -# application/samlmetadata+xml -application/sbml+xml sbml -application/scvp-cv-request scq -application/scvp-cv-response scs -application/scvp-vp-request spq -application/scvp-vp-response spp -application/sdp sdp -# application/set-payment -application/set-payment-initiation setpay -# application/set-registration -application/set-registration-initiation setreg -# application/sgml -# application/sgml-open-catalog -application/shf+xml shf -# application/sieve -# application/simple-filter+xml -# application/simple-message-summary -# application/simplesymbolcontainer -# application/slate -# application/smil -application/smil+xml smi smil -# application/soap+fastinfoset -# application/soap+xml -application/sparql-query rq -application/sparql-results+xml srx -# application/spirits-event+xml -application/srgs gram -application/srgs+xml grxml -application/sru+xml sru -application/ssdl+xml ssdl -application/ssml+xml ssml -# application/tamp-apex-update -# application/tamp-apex-update-confirm -# application/tamp-community-update -# application/tamp-community-update-confirm -# application/tamp-error -# application/tamp-sequence-adjust -# application/tamp-sequence-adjust-confirm -# application/tamp-status-query -# application/tamp-status-response -# application/tamp-update -# application/tamp-update-confirm -application/tei+xml tei teicorpus -application/thraud+xml tfi -# application/timestamp-query -# application/timestamp-reply -application/timestamped-data tsd -# application/tve-trigger -# application/ulpfec -# application/vcard+xml -# application/vemmi -# application/vividence.scriptfile -# application/vnd.3gpp.bsf+xml -application/vnd.3gpp.pic-bw-large plb -application/vnd.3gpp.pic-bw-small psb -application/vnd.3gpp.pic-bw-var pvb -# application/vnd.3gpp.sms -# application/vnd.3gpp2.bcmcsinfo+xml -# application/vnd.3gpp2.sms -application/vnd.3gpp2.tcap tcap -application/vnd.3m.post-it-notes pwn -application/vnd.accpac.simply.aso aso -application/vnd.accpac.simply.imp imp -application/vnd.acucobol acu -application/vnd.acucorp atc acutc -application/vnd.adobe.air-application-installer-package+zip air -application/vnd.adobe.formscentral.fcdt fcdt -application/vnd.adobe.fxp fxp fxpl -# application/vnd.adobe.partial-upload -application/vnd.adobe.xdp+xml xdp -application/vnd.adobe.xfdf xfdf -# application/vnd.aether.imp -# application/vnd.ah-barcode -application/vnd.ahead.space ahead -application/vnd.airzip.filesecure.azf azf -application/vnd.airzip.filesecure.azs azs -application/vnd.amazon.ebook azw -application/vnd.americandynamics.acc acc -application/vnd.amiga.ami ami -# application/vnd.amundsen.maze+xml -application/vnd.android.package-archive apk -application/vnd.anser-web-certificate-issue-initiation cii -application/vnd.anser-web-funds-transfer-initiation fti -application/vnd.antix.game-component atx -application/vnd.apple.installer+xml mpkg -application/vnd.apple.mpegurl m3u8 -# application/vnd.arastra.swi -application/vnd.aristanetworks.swi swi -application/vnd.astraea-software.iota iota -application/vnd.audiograph aep -# application/vnd.autopackage -# application/vnd.avistar+xml -application/vnd.blueice.multipass mpm -# application/vnd.bluetooth.ep.oob -application/vnd.bmi bmi -application/vnd.businessobjects rep -# application/vnd.cab-jscript -# application/vnd.canon-cpdl -# application/vnd.canon-lips -# application/vnd.cendio.thinlinc.clientconf -application/vnd.chemdraw+xml cdxml -application/vnd.chipnuts.karaoke-mmd mmd -application/vnd.cinderella cdy -# application/vnd.cirpack.isdn-ext -application/vnd.claymore cla -application/vnd.cloanto.rp9 rp9 -application/vnd.clonk.c4group c4g c4d c4f c4p c4u -application/vnd.cluetrust.cartomobile-config c11amc -application/vnd.cluetrust.cartomobile-config-pkg c11amz -# application/vnd.collection+json -# application/vnd.commerce-battelle -application/vnd.commonspace csp -application/vnd.contact.cmsg cdbcmsg -application/vnd.cosmocaller cmc -application/vnd.crick.clicker clkx -application/vnd.crick.clicker.keyboard clkk -application/vnd.crick.clicker.palette clkp -application/vnd.crick.clicker.template clkt -application/vnd.crick.clicker.wordbank clkw -application/vnd.criticaltools.wbs+xml wbs -application/vnd.ctc-posml pml -# application/vnd.ctct.ws+xml -# application/vnd.cups-pdf -# application/vnd.cups-postscript -application/vnd.cups-ppd ppd -# application/vnd.cups-raster -# application/vnd.cups-raw -# application/vnd.curl -application/vnd.curl.car car -application/vnd.curl.pcurl pcurl -# application/vnd.cybank -application/vnd.dart dart -application/vnd.data-vision.rdz rdz -application/vnd.dece.data uvf uvvf uvd uvvd -application/vnd.dece.ttml+xml uvt uvvt -application/vnd.dece.unspecified uvx uvvx -application/vnd.dece.zip uvz uvvz -application/vnd.denovo.fcselayout-link fe_launch -# application/vnd.dir-bi.plate-dl-nosuffix -application/vnd.dna dna -application/vnd.dolby.mlp mlp -# application/vnd.dolby.mobile.1 -# application/vnd.dolby.mobile.2 -application/vnd.dpgraph dpg -application/vnd.dreamfactory dfac -application/vnd.ds-keypoint kpxx -application/vnd.dvb.ait ait -# application/vnd.dvb.dvbj -# application/vnd.dvb.esgcontainer -# application/vnd.dvb.ipdcdftnotifaccess -# application/vnd.dvb.ipdcesgaccess -# application/vnd.dvb.ipdcesgaccess2 -# application/vnd.dvb.ipdcesgpdd -# application/vnd.dvb.ipdcroaming -# application/vnd.dvb.iptv.alfec-base -# application/vnd.dvb.iptv.alfec-enhancement -# application/vnd.dvb.notif-aggregate-root+xml -# application/vnd.dvb.notif-container+xml -# application/vnd.dvb.notif-generic+xml -# application/vnd.dvb.notif-ia-msglist+xml -# application/vnd.dvb.notif-ia-registration-request+xml -# application/vnd.dvb.notif-ia-registration-response+xml -# application/vnd.dvb.notif-init+xml -# application/vnd.dvb.pfr -application/vnd.dvb.service svc -# application/vnd.dxr -application/vnd.dynageo geo -# application/vnd.easykaraoke.cdgdownload -# application/vnd.ecdis-update -application/vnd.ecowin.chart mag -# application/vnd.ecowin.filerequest -# application/vnd.ecowin.fileupdate -# application/vnd.ecowin.series -# application/vnd.ecowin.seriesrequest -# application/vnd.ecowin.seriesupdate -# application/vnd.emclient.accessrequest+xml -application/vnd.enliven nml -# application/vnd.eprints.data+xml -application/vnd.epson.esf esf -application/vnd.epson.msf msf -application/vnd.epson.quickanime qam -application/vnd.epson.salt slt -application/vnd.epson.ssf ssf -# application/vnd.ericsson.quickcall -application/vnd.eszigno3+xml es3 et3 -# application/vnd.etsi.aoc+xml -# application/vnd.etsi.cug+xml -# application/vnd.etsi.iptvcommand+xml -# application/vnd.etsi.iptvdiscovery+xml -# application/vnd.etsi.iptvprofile+xml -# application/vnd.etsi.iptvsad-bc+xml -# application/vnd.etsi.iptvsad-cod+xml -# application/vnd.etsi.iptvsad-npvr+xml -# application/vnd.etsi.iptvservice+xml -# application/vnd.etsi.iptvsync+xml -# application/vnd.etsi.iptvueprofile+xml -# application/vnd.etsi.mcid+xml -# application/vnd.etsi.overload-control-policy-dataset+xml -# application/vnd.etsi.sci+xml -# application/vnd.etsi.simservs+xml -# application/vnd.etsi.tsl+xml -# application/vnd.etsi.tsl.der -# application/vnd.eudora.data -application/vnd.ezpix-album ez2 -application/vnd.ezpix-package ez3 -# application/vnd.f-secure.mobile -application/vnd.fdf fdf -application/vnd.fdsn.mseed mseed -application/vnd.fdsn.seed seed dataless -# application/vnd.ffsns -# application/vnd.fints -application/vnd.flographit gph -application/vnd.fluxtime.clip ftc -# application/vnd.font-fontforge-sfd -application/vnd.framemaker fm frame maker book -application/vnd.frogans.fnc fnc -application/vnd.frogans.ltf ltf -application/vnd.fsc.weblaunch fsc -application/vnd.fujitsu.oasys oas -application/vnd.fujitsu.oasys2 oa2 -application/vnd.fujitsu.oasys3 oa3 -application/vnd.fujitsu.oasysgp fg5 -application/vnd.fujitsu.oasysprs bh2 -# application/vnd.fujixerox.art-ex -# application/vnd.fujixerox.art4 -# application/vnd.fujixerox.hbpl -application/vnd.fujixerox.ddd ddd -application/vnd.fujixerox.docuworks xdw -application/vnd.fujixerox.docuworks.binder xbd -# application/vnd.fut-misnet -application/vnd.fuzzysheet fzs -application/vnd.genomatix.tuxedo txd -# application/vnd.geocube+xml -application/vnd.geogebra.file ggb -application/vnd.geogebra.tool ggt -application/vnd.geometry-explorer gex gre -application/vnd.geonext gxt -application/vnd.geoplan g2w -application/vnd.geospace g3w -# application/vnd.globalplatform.card-content-mgt -# application/vnd.globalplatform.card-content-mgt-response -application/vnd.gmx gmx -application/vnd.google-earth.kml+xml kml -application/vnd.google-earth.kmz kmz -application/vnd.grafeq gqf gqs -# application/vnd.gridmp -application/vnd.groove-account gac -application/vnd.groove-help ghf -application/vnd.groove-identity-message gim -application/vnd.groove-injector grv -application/vnd.groove-tool-message gtm -application/vnd.groove-tool-template tpl -application/vnd.groove-vcard vcg -# application/vnd.hal+json -application/vnd.hal+xml hal -application/vnd.handheld-entertainment+xml zmm -application/vnd.hbci hbci -# application/vnd.hcl-bireports -application/vnd.hhe.lesson-player les -application/vnd.hp-hpgl hpgl -application/vnd.hp-hpid hpid -application/vnd.hp-hps hps -application/vnd.hp-jlyt jlt -application/vnd.hp-pcl pcl -application/vnd.hp-pclxl pclxl -# application/vnd.httphone -application/vnd.hydrostatix.sof-data sfd-hdstx -# application/vnd.hzn-3d-crossword -# application/vnd.ibm.afplinedata -# application/vnd.ibm.electronic-media -application/vnd.ibm.minipay mpy -application/vnd.ibm.modcap afp listafp list3820 -application/vnd.ibm.rights-management irm -application/vnd.ibm.secure-container sc -application/vnd.iccprofile icc icm -application/vnd.igloader igl -application/vnd.immervision-ivp ivp -application/vnd.immervision-ivu ivu -# application/vnd.informedcontrol.rms+xml -# application/vnd.informix-visionary -# application/vnd.infotech.project -# application/vnd.infotech.project+xml -# application/vnd.innopath.wamp.notification -application/vnd.insors.igm igm -application/vnd.intercon.formnet xpw xpx -application/vnd.intergeo i2g -# application/vnd.intertrust.digibox -# application/vnd.intertrust.nncp -application/vnd.intu.qbo qbo -application/vnd.intu.qfx qfx -# application/vnd.iptc.g2.conceptitem+xml -# application/vnd.iptc.g2.knowledgeitem+xml -# application/vnd.iptc.g2.newsitem+xml -# application/vnd.iptc.g2.newsmessage+xml -# application/vnd.iptc.g2.packageitem+xml -# application/vnd.iptc.g2.planningitem+xml -application/vnd.ipunplugged.rcprofile rcprofile -application/vnd.irepository.package+xml irp -application/vnd.is-xpr xpr -application/vnd.isac.fcs fcs -application/vnd.jam jam -# application/vnd.japannet-directory-service -# application/vnd.japannet-jpnstore-wakeup -# application/vnd.japannet-payment-wakeup -# application/vnd.japannet-registration -# application/vnd.japannet-registration-wakeup -# application/vnd.japannet-setstore-wakeup -# application/vnd.japannet-verification -# application/vnd.japannet-verification-wakeup -application/vnd.jcp.javame.midlet-rms rms -application/vnd.jisp jisp -application/vnd.joost.joda-archive joda -application/vnd.kahootz ktz ktr -application/vnd.kde.karbon karbon -application/vnd.kde.kchart chrt -application/vnd.kde.kformula kfo -application/vnd.kde.kivio flw -application/vnd.kde.kontour kon -application/vnd.kde.kpresenter kpr kpt -application/vnd.kde.kspread ksp -application/vnd.kde.kword kwd kwt -application/vnd.kenameaapp htke -application/vnd.kidspiration kia -application/vnd.kinar kne knp -application/vnd.koan skp skd skt skm -application/vnd.kodak-descriptor sse -application/vnd.las.las+xml lasxml -# application/vnd.liberty-request+xml -application/vnd.llamagraphics.life-balance.desktop lbd -application/vnd.llamagraphics.life-balance.exchange+xml lbe -application/vnd.lotus-1-2-3 123 -application/vnd.lotus-approach apr -application/vnd.lotus-freelance pre -application/vnd.lotus-notes nsf -application/vnd.lotus-organizer org -application/vnd.lotus-screencam scm -application/vnd.lotus-wordpro lwp -application/vnd.macports.portpkg portpkg -# application/vnd.marlin.drm.actiontoken+xml -# application/vnd.marlin.drm.conftoken+xml -# application/vnd.marlin.drm.license+xml -# application/vnd.marlin.drm.mdcf -application/vnd.mcd mcd -application/vnd.medcalcdata mc1 -application/vnd.mediastation.cdkey cdkey -# application/vnd.meridian-slingshot -application/vnd.mfer mwf -application/vnd.mfmp mfm -application/vnd.micrografx.flo flo -application/vnd.micrografx.igx igx -application/vnd.mif mif -# application/vnd.minisoft-hp3000-save -# application/vnd.mitsubishi.misty-guard.trustweb -application/vnd.mobius.daf daf -application/vnd.mobius.dis dis -application/vnd.mobius.mbk mbk -application/vnd.mobius.mqy mqy -application/vnd.mobius.msl msl -application/vnd.mobius.plc plc -application/vnd.mobius.txf txf -application/vnd.mophun.application mpn -application/vnd.mophun.certificate mpc -# application/vnd.motorola.flexsuite -# application/vnd.motorola.flexsuite.adsi -# application/vnd.motorola.flexsuite.fis -# application/vnd.motorola.flexsuite.gotap -# application/vnd.motorola.flexsuite.kmr -# application/vnd.motorola.flexsuite.ttc -# application/vnd.motorola.flexsuite.wem -# application/vnd.motorola.iprm -application/vnd.mozilla.xul+xml xul -application/vnd.ms-artgalry cil -# application/vnd.ms-asf -application/vnd.ms-cab-compressed cab -# application/vnd.ms-color.iccprofile -application/vnd.ms-excel xls xlm xla xlc xlt xlw -application/vnd.ms-excel.addin.macroenabled.12 xlam -application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb -application/vnd.ms-excel.sheet.macroenabled.12 xlsm -application/vnd.ms-excel.template.macroenabled.12 xltm -application/vnd.ms-fontobject eot -application/vnd.ms-htmlhelp chm -application/vnd.ms-ims ims -application/vnd.ms-lrm lrm -# application/vnd.ms-office.activex+xml -application/vnd.ms-officetheme thmx -# application/vnd.ms-opentype -# application/vnd.ms-package.obfuscated-opentype -application/vnd.ms-pki.seccat cat -application/vnd.ms-pki.stl stl -# application/vnd.ms-playready.initiator+xml -application/vnd.ms-powerpoint ppt pps pot -application/vnd.ms-powerpoint.addin.macroenabled.12 ppam -application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm -application/vnd.ms-powerpoint.slide.macroenabled.12 sldm -application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm -application/vnd.ms-powerpoint.template.macroenabled.12 potm -# application/vnd.ms-printing.printticket+xml -application/vnd.ms-project mpp mpt -# application/vnd.ms-tnef -# application/vnd.ms-wmdrm.lic-chlg-req -# application/vnd.ms-wmdrm.lic-resp -# application/vnd.ms-wmdrm.meter-chlg-req -# application/vnd.ms-wmdrm.meter-resp -application/vnd.ms-word.document.macroenabled.12 docm -application/vnd.ms-word.template.macroenabled.12 dotm -application/vnd.ms-works wps wks wcm wdb -application/vnd.ms-wpl wpl -application/vnd.ms-xpsdocument xps -application/vnd.mseq mseq -# application/vnd.msign -# application/vnd.multiad.creator -# application/vnd.multiad.creator.cif -# application/vnd.music-niff -application/vnd.musician mus -application/vnd.muvee.style msty -application/vnd.mynfc taglet -# application/vnd.ncd.control -# application/vnd.ncd.reference -# application/vnd.nervana -# application/vnd.netfpx -application/vnd.neurolanguage.nlu nlu -application/vnd.nitf ntf nitf -application/vnd.noblenet-directory nnd -application/vnd.noblenet-sealer nns -application/vnd.noblenet-web nnw -# application/vnd.nokia.catalogs -# application/vnd.nokia.conml+wbxml -# application/vnd.nokia.conml+xml -# application/vnd.nokia.isds-radio-presets -# application/vnd.nokia.iptv.config+xml -# application/vnd.nokia.landmark+wbxml -# application/vnd.nokia.landmark+xml -# application/vnd.nokia.landmarkcollection+xml -# application/vnd.nokia.n-gage.ac+xml -application/vnd.nokia.n-gage.data ngdat -application/vnd.nokia.n-gage.symbian.install n-gage -# application/vnd.nokia.ncd -# application/vnd.nokia.pcd+wbxml -# application/vnd.nokia.pcd+xml -application/vnd.nokia.radio-preset rpst -application/vnd.nokia.radio-presets rpss -application/vnd.novadigm.edm edm -application/vnd.novadigm.edx edx -application/vnd.novadigm.ext ext -# application/vnd.ntt-local.file-transfer -# application/vnd.ntt-local.sip-ta_remote -# application/vnd.ntt-local.sip-ta_tcp_stream -application/vnd.oasis.opendocument.chart odc -application/vnd.oasis.opendocument.chart-template otc -application/vnd.oasis.opendocument.database odb -application/vnd.oasis.opendocument.formula odf -application/vnd.oasis.opendocument.formula-template odft -application/vnd.oasis.opendocument.graphics odg -application/vnd.oasis.opendocument.graphics-template otg -application/vnd.oasis.opendocument.image odi -application/vnd.oasis.opendocument.image-template oti -application/vnd.oasis.opendocument.presentation odp -application/vnd.oasis.opendocument.presentation-template otp -application/vnd.oasis.opendocument.spreadsheet ods -application/vnd.oasis.opendocument.spreadsheet-template ots -application/vnd.oasis.opendocument.text odt -application/vnd.oasis.opendocument.text-master odm -application/vnd.oasis.opendocument.text-template ott -application/vnd.oasis.opendocument.text-web oth -# application/vnd.obn -# application/vnd.oftn.l10n+json -# application/vnd.oipf.contentaccessdownload+xml -# application/vnd.oipf.contentaccessstreaming+xml -# application/vnd.oipf.cspg-hexbinary -# application/vnd.oipf.dae.svg+xml -# application/vnd.oipf.dae.xhtml+xml -# application/vnd.oipf.mippvcontrolmessage+xml -# application/vnd.oipf.pae.gem -# application/vnd.oipf.spdiscovery+xml -# application/vnd.oipf.spdlist+xml -# application/vnd.oipf.ueprofile+xml -# application/vnd.oipf.userprofile+xml -application/vnd.olpc-sugar xo -# application/vnd.oma-scws-config -# application/vnd.oma-scws-http-request -# application/vnd.oma-scws-http-response -# application/vnd.oma.bcast.associated-procedure-parameter+xml -# application/vnd.oma.bcast.drm-trigger+xml -# application/vnd.oma.bcast.imd+xml -# application/vnd.oma.bcast.ltkm -# application/vnd.oma.bcast.notification+xml -# application/vnd.oma.bcast.provisioningtrigger -# application/vnd.oma.bcast.sgboot -# application/vnd.oma.bcast.sgdd+xml -# application/vnd.oma.bcast.sgdu -# application/vnd.oma.bcast.simple-symbol-container -# application/vnd.oma.bcast.smartcard-trigger+xml -# application/vnd.oma.bcast.sprov+xml -# application/vnd.oma.bcast.stkm -# application/vnd.oma.cab-address-book+xml -# application/vnd.oma.cab-feature-handler+xml -# application/vnd.oma.cab-pcc+xml -# application/vnd.oma.cab-user-prefs+xml -# application/vnd.oma.dcd -# application/vnd.oma.dcdc -application/vnd.oma.dd2+xml dd2 -# application/vnd.oma.drm.risd+xml -# application/vnd.oma.group-usage-list+xml -# application/vnd.oma.pal+xml -# application/vnd.oma.poc.detailed-progress-report+xml -# application/vnd.oma.poc.final-report+xml -# application/vnd.oma.poc.groups+xml -# application/vnd.oma.poc.invocation-descriptor+xml -# application/vnd.oma.poc.optimized-progress-report+xml -# application/vnd.oma.push -# application/vnd.oma.scidm.messages+xml -# application/vnd.oma.xcap-directory+xml -# application/vnd.omads-email+xml -# application/vnd.omads-file+xml -# application/vnd.omads-folder+xml -# application/vnd.omaloc-supl-init -application/vnd.openofficeorg.extension oxt -# application/vnd.openxmlformats-officedocument.custom-properties+xml -# application/vnd.openxmlformats-officedocument.customxmlproperties+xml -# application/vnd.openxmlformats-officedocument.drawing+xml -# application/vnd.openxmlformats-officedocument.drawingml.chart+xml -# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml -# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml -# application/vnd.openxmlformats-officedocument.extended-properties+xml -# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml -# application/vnd.openxmlformats-officedocument.presentationml.comments+xml -# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml -# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml -application/vnd.openxmlformats-officedocument.presentationml.presentation pptx -# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml -application/vnd.openxmlformats-officedocument.presentationml.slide sldx -# application/vnd.openxmlformats-officedocument.presentationml.slide+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml -# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml -application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx -# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml -# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml -# application/vnd.openxmlformats-officedocument.presentationml.tags+xml -application/vnd.openxmlformats-officedocument.presentationml.template potx -# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml -# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml -application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx -# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml -# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml -# application/vnd.openxmlformats-officedocument.theme+xml -# application/vnd.openxmlformats-officedocument.themeoverride+xml -# application/vnd.openxmlformats-officedocument.vmldrawing -# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.document docx -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml -application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx -# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml -# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml -# application/vnd.openxmlformats-package.core-properties+xml -# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml -# application/vnd.openxmlformats-package.relationships+xml -# application/vnd.quobject-quoxdocument -# application/vnd.osa.netdeploy -application/vnd.osgeo.mapguide.package mgp -# application/vnd.osgi.bundle -application/vnd.osgi.dp dp -application/vnd.osgi.subsystem esa -# application/vnd.otps.ct-kip+xml -application/vnd.palm pdb pqa oprc -# application/vnd.paos.xml -application/vnd.pawaafile paw -application/vnd.pg.format str -application/vnd.pg.osasli ei6 -# application/vnd.piaccess.application-licence -application/vnd.picsel efif -application/vnd.pmi.widget wg -# application/vnd.poc.group-advertisement+xml -application/vnd.pocketlearn plf -application/vnd.powerbuilder6 pbd -# application/vnd.powerbuilder6-s -# application/vnd.powerbuilder7 -# application/vnd.powerbuilder7-s -# application/vnd.powerbuilder75 -# application/vnd.powerbuilder75-s -# application/vnd.preminet -application/vnd.previewsystems.box box -application/vnd.proteus.magazine mgz -application/vnd.publishare-delta-tree qps -application/vnd.pvi.ptid1 ptid -# application/vnd.pwg-multiplexed -# application/vnd.pwg-xhtml-print+xml -# application/vnd.qualcomm.brew-app-res -application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb -# application/vnd.radisys.moml+xml -# application/vnd.radisys.msml+xml -# application/vnd.radisys.msml-audit+xml -# application/vnd.radisys.msml-audit-conf+xml -# application/vnd.radisys.msml-audit-conn+xml -# application/vnd.radisys.msml-audit-dialog+xml -# application/vnd.radisys.msml-audit-stream+xml -# application/vnd.radisys.msml-conf+xml -# application/vnd.radisys.msml-dialog+xml -# application/vnd.radisys.msml-dialog-base+xml -# application/vnd.radisys.msml-dialog-fax-detect+xml -# application/vnd.radisys.msml-dialog-fax-sendrecv+xml -# application/vnd.radisys.msml-dialog-group+xml -# application/vnd.radisys.msml-dialog-speech+xml -# application/vnd.radisys.msml-dialog-transform+xml -# application/vnd.rainstor.data -# application/vnd.rapid -application/vnd.realvnc.bed bed -application/vnd.recordare.musicxml mxl -application/vnd.recordare.musicxml+xml musicxml -# application/vnd.renlearn.rlprint -application/vnd.rig.cryptonote cryptonote -application/vnd.rim.cod cod -application/vnd.rn-realmedia rm -application/vnd.rn-realmedia-vbr rmvb -application/vnd.route66.link66+xml link66 -# application/vnd.rs-274x -# application/vnd.ruckus.download -# application/vnd.s3sms -application/vnd.sailingtracker.track st -# application/vnd.sbm.cid -# application/vnd.sbm.mid2 -# application/vnd.scribus -# application/vnd.sealed.3df -# application/vnd.sealed.csf -# application/vnd.sealed.doc -# application/vnd.sealed.eml -# application/vnd.sealed.mht -# application/vnd.sealed.net -# application/vnd.sealed.ppt -# application/vnd.sealed.tiff -# application/vnd.sealed.xls -# application/vnd.sealedmedia.softseal.html -# application/vnd.sealedmedia.softseal.pdf -application/vnd.seemail see -application/vnd.sema sema -application/vnd.semd semd -application/vnd.semf semf -application/vnd.shana.informed.formdata ifm -application/vnd.shana.informed.formtemplate itp -application/vnd.shana.informed.interchange iif -application/vnd.shana.informed.package ipk -application/vnd.simtech-mindmapper twd twds -application/vnd.smaf mmf -# application/vnd.smart.notebook -application/vnd.smart.teacher teacher -# application/vnd.software602.filler.form+xml -# application/vnd.software602.filler.form-xml-zip -application/vnd.solent.sdkm+xml sdkm sdkd -application/vnd.spotfire.dxp dxp -application/vnd.spotfire.sfs sfs -# application/vnd.sss-cod -# application/vnd.sss-dtf -# application/vnd.sss-ntf -application/vnd.stardivision.calc sdc -application/vnd.stardivision.draw sda -application/vnd.stardivision.impress sdd -application/vnd.stardivision.math smf -application/vnd.stardivision.writer sdw vor -application/vnd.stardivision.writer-global sgl -application/vnd.stepmania.package smzip -application/vnd.stepmania.stepchart sm -# application/vnd.street-stream -application/vnd.sun.xml.calc sxc -application/vnd.sun.xml.calc.template stc -application/vnd.sun.xml.draw sxd -application/vnd.sun.xml.draw.template std -application/vnd.sun.xml.impress sxi -application/vnd.sun.xml.impress.template sti -application/vnd.sun.xml.math sxm -application/vnd.sun.xml.writer sxw -application/vnd.sun.xml.writer.global sxg -application/vnd.sun.xml.writer.template stw -# application/vnd.sun.wadl+xml -application/vnd.sus-calendar sus susp -application/vnd.svd svd -# application/vnd.swiftview-ics -application/vnd.symbian.install sis sisx -application/vnd.syncml+xml xsm -application/vnd.syncml.dm+wbxml bdm -application/vnd.syncml.dm+xml xdm -# application/vnd.syncml.dm.notification -# application/vnd.syncml.ds.notification -application/vnd.tao.intent-module-archive tao -application/vnd.tcpdump.pcap pcap cap dmp -application/vnd.tmobile-livetv tmo -application/vnd.trid.tpt tpt -application/vnd.triscape.mxs mxs -application/vnd.trueapp tra -# application/vnd.truedoc -# application/vnd.ubisoft.webplayer -application/vnd.ufdl ufd ufdl -application/vnd.uiq.theme utz -application/vnd.umajin umj -application/vnd.unity unityweb -application/vnd.uoml+xml uoml -# application/vnd.uplanet.alert -# application/vnd.uplanet.alert-wbxml -# application/vnd.uplanet.bearer-choice -# application/vnd.uplanet.bearer-choice-wbxml -# application/vnd.uplanet.cacheop -# application/vnd.uplanet.cacheop-wbxml -# application/vnd.uplanet.channel -# application/vnd.uplanet.channel-wbxml -# application/vnd.uplanet.list -# application/vnd.uplanet.list-wbxml -# application/vnd.uplanet.listcmd -# application/vnd.uplanet.listcmd-wbxml -# application/vnd.uplanet.signal -application/vnd.vcx vcx -# application/vnd.vd-study -# application/vnd.vectorworks -# application/vnd.verimatrix.vcas -# application/vnd.vidsoft.vidconference -application/vnd.visio vsd vst vss vsw -application/vnd.visionary vis -# application/vnd.vividence.scriptfile -application/vnd.vsf vsf -# application/vnd.wap.sic -# application/vnd.wap.slc -application/vnd.wap.wbxml wbxml -application/vnd.wap.wmlc wmlc -application/vnd.wap.wmlscriptc wmlsc -application/vnd.webturbo wtb -# application/vnd.wfa.wsc -# application/vnd.wmc -# application/vnd.wmf.bootstrap -# application/vnd.wolfram.mathematica -# application/vnd.wolfram.mathematica.package -application/vnd.wolfram.player nbp -application/vnd.wordperfect wpd -application/vnd.wqd wqd -# application/vnd.wrq-hp3000-labelled -application/vnd.wt.stf stf -# application/vnd.wv.csp+wbxml -# application/vnd.wv.csp+xml -# application/vnd.wv.ssp+xml -application/vnd.xara xar -application/vnd.xfdl xfdl -# application/vnd.xfdl.webform -# application/vnd.xmi+xml -# application/vnd.xmpie.cpkg -# application/vnd.xmpie.dpkg -# application/vnd.xmpie.plan -# application/vnd.xmpie.ppkg -# application/vnd.xmpie.xlim -application/vnd.yamaha.hv-dic hvd -application/vnd.yamaha.hv-script hvs -application/vnd.yamaha.hv-voice hvp -application/vnd.yamaha.openscoreformat osf -application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg -# application/vnd.yamaha.remote-setup -application/vnd.yamaha.smaf-audio saf -application/vnd.yamaha.smaf-phrase spf -# application/vnd.yamaha.through-ngn -# application/vnd.yamaha.tunnel-udpencap -application/vnd.yellowriver-custom-menu cmp -application/vnd.zul zir zirz -application/vnd.zzazz.deck+xml zaz -application/voicexml+xml vxml -# application/vq-rtcpxr -# application/watcherinfo+xml -# application/whoispp-query -# application/whoispp-response -application/widget wgt -application/winhlp hlp -# application/wita -# application/wordperfect5.1 -application/wsdl+xml wsdl -application/wspolicy+xml wspolicy -application/x-7z-compressed 7z -application/x-abiword abw -application/x-ace-compressed ace -# application/x-amf -application/x-apple-diskimage dmg -application/x-authorware-bin aab x32 u32 vox -application/x-authorware-map aam -application/x-authorware-seg aas -application/x-bcpio bcpio -application/x-bittorrent torrent -application/x-blorb blb blorb -application/x-bzip bz -application/x-bzip2 bz2 boz -application/x-cbr cbr cba cbt cbz cb7 -application/x-cdlink vcd -application/x-cfs-compressed cfs -application/x-chat chat -application/x-chess-pgn pgn -application/x-conference nsc -# application/x-compress -application/x-cpio cpio -application/x-csh csh -application/x-debian-package deb udeb -application/x-dgc-compressed dgc -application/x-director dir dcr dxr cst cct cxt w3d fgd swa -application/x-doom wad -application/x-dtbncx+xml ncx -application/x-dtbook+xml dtb -application/x-dtbresource+xml res -application/x-dvi dvi -application/x-envoy evy -application/x-eva eva -application/x-font-bdf bdf -# application/x-font-dos -# application/x-font-framemaker -application/x-font-ghostscript gsf -# application/x-font-libgrx -application/x-font-linux-psf psf -application/x-font-otf otf -application/x-font-pcf pcf -application/x-font-snf snf -# application/x-font-speedo -# application/x-font-sunos-news -application/x-font-ttf ttf ttc -application/x-font-type1 pfa pfb pfm afm -application/x-font-woff woff -# application/x-font-vfont -application/x-freearc arc -application/x-futuresplash spl -application/x-gca-compressed gca -application/x-glulx ulx -application/x-gnumeric gnumeric -application/x-gramps-xml gramps -application/x-gtar gtar -# application/x-gzip -application/x-hdf hdf -application/x-install-instructions install -application/x-iso9660-image iso -application/x-java-jnlp-file jnlp -application/x-latex latex -application/x-lzh-compressed lzh lha -application/x-mie mie -application/x-mobipocket-ebook prc mobi -application/x-ms-application application -application/x-ms-shortcut lnk -application/x-ms-wmd wmd -application/x-ms-wmz wmz -application/x-ms-xbap xbap -application/x-msaccess mdb -application/x-msbinder obd -application/x-mscardfile crd -application/x-msclip clp -application/x-msdownload exe dll com bat msi -application/x-msmediaview mvb m13 m14 -application/x-msmetafile wmf wmz emf emz -application/x-msmoney mny -application/x-mspublisher pub -application/x-msschedule scd -application/x-msterminal trm -application/x-mswrite wri -application/x-netcdf nc cdf -application/x-nzb nzb -application/x-pkcs12 p12 pfx -application/x-pkcs7-certificates p7b spc -application/x-pkcs7-certreqresp p7r -application/x-rar-compressed rar -application/x-research-info-systems ris -application/x-sh sh -application/x-shar shar -application/x-shockwave-flash swf -application/x-silverlight-app xap -application/x-sql sql -application/x-stuffit sit -application/x-stuffitx sitx -application/x-subrip srt -application/x-sv4cpio sv4cpio -application/x-sv4crc sv4crc -application/x-t3vm-image t3 -application/x-tads gam -application/x-tar tar -application/x-tcl tcl -application/x-tex tex -application/x-tex-tfm tfm -application/x-texinfo texinfo texi -application/x-tgif obj -application/x-ustar ustar -application/x-wais-source src -application/x-x509-ca-cert der crt -application/x-xfig fig -application/x-xliff+xml xlf -application/x-xpinstall xpi -application/x-xz xz -application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8 -# application/x400-bp -application/xaml+xml xaml -# application/xcap-att+xml -# application/xcap-caps+xml -application/xcap-diff+xml xdf -# application/xcap-el+xml -# application/xcap-error+xml -# application/xcap-ns+xml -# application/xcon-conference-info-diff+xml -# application/xcon-conference-info+xml -application/xenc+xml xenc -application/xhtml+xml xhtml xht -# application/xhtml-voice+xml -application/xml xml xsl -application/xml-dtd dtd -# application/xml-external-parsed-entity -# application/xmpp+xml -application/xop+xml xop -application/xproc+xml xpl -application/xslt+xml xslt -application/xspf+xml xspf -application/xv+xml mxml xhvml xvml xvm -application/yang yang -application/yin+xml yin -application/zip zip -# audio/1d-interleaved-parityfec -# audio/32kadpcm -# audio/3gpp -# audio/3gpp2 -# audio/ac3 -audio/adpcm adp -# audio/amr -# audio/amr-wb -# audio/amr-wb+ -# audio/asc -# audio/atrac-advanced-lossless -# audio/atrac-x -# audio/atrac3 -audio/basic au snd -# audio/bv16 -# audio/bv32 -# audio/clearmode -# audio/cn -# audio/dat12 -# audio/dls -# audio/dsr-es201108 -# audio/dsr-es202050 -# audio/dsr-es202211 -# audio/dsr-es202212 -# audio/dv -# audio/dvi4 -# audio/eac3 -# audio/evrc -# audio/evrc-qcp -# audio/evrc0 -# audio/evrc1 -# audio/evrcb -# audio/evrcb0 -# audio/evrcb1 -# audio/evrcwb -# audio/evrcwb0 -# audio/evrcwb1 -# audio/example -# audio/fwdred -# audio/g719 -# audio/g722 -# audio/g7221 -# audio/g723 -# audio/g726-16 -# audio/g726-24 -# audio/g726-32 -# audio/g726-40 -# audio/g728 -# audio/g729 -# audio/g7291 -# audio/g729d -# audio/g729e -# audio/gsm -# audio/gsm-efr -# audio/gsm-hr-08 -# audio/ilbc -# audio/ip-mr_v2.5 -# audio/isac -# audio/l16 -# audio/l20 -# audio/l24 -# audio/l8 -# audio/lpc -audio/midi mid midi kar rmi -# audio/mobile-xmf -audio/mp4 mp4a -# audio/mp4a-latm -# audio/mpa -# audio/mpa-robust -audio/mpeg mpga mp2 mp2a mp3 m2a m3a -# audio/mpeg4-generic -# audio/musepack -audio/ogg oga ogg spx -# audio/opus -# audio/parityfec -# audio/pcma -# audio/pcma-wb -# audio/pcmu-wb -# audio/pcmu -# audio/prs.sid -# audio/qcelp -# audio/red -# audio/rtp-enc-aescm128 -# audio/rtp-midi -# audio/rtx -audio/s3m s3m -audio/silk sil -# audio/smv -# audio/smv0 -# audio/smv-qcp -# audio/sp-midi -# audio/speex -# audio/t140c -# audio/t38 -# audio/telephone-event -# audio/tone -# audio/uemclip -# audio/ulpfec -# audio/vdvi -# audio/vmr-wb -# audio/vnd.3gpp.iufp -# audio/vnd.4sb -# audio/vnd.audiokoz -# audio/vnd.celp -# audio/vnd.cisco.nse -# audio/vnd.cmles.radio-events -# audio/vnd.cns.anp1 -# audio/vnd.cns.inf1 -audio/vnd.dece.audio uva uvva -audio/vnd.digital-winds eol -# audio/vnd.dlna.adts -# audio/vnd.dolby.heaac.1 -# audio/vnd.dolby.heaac.2 -# audio/vnd.dolby.mlp -# audio/vnd.dolby.mps -# audio/vnd.dolby.pl2 -# audio/vnd.dolby.pl2x -# audio/vnd.dolby.pl2z -# audio/vnd.dolby.pulse.1 -audio/vnd.dra dra -audio/vnd.dts dts -audio/vnd.dts.hd dtshd -# audio/vnd.dvb.file -# audio/vnd.everad.plj -# audio/vnd.hns.audio -audio/vnd.lucent.voice lvp -audio/vnd.ms-playready.media.pya pya -# audio/vnd.nokia.mobile-xmf -# audio/vnd.nortel.vbk -audio/vnd.nuera.ecelp4800 ecelp4800 -audio/vnd.nuera.ecelp7470 ecelp7470 -audio/vnd.nuera.ecelp9600 ecelp9600 -# audio/vnd.octel.sbc -# audio/vnd.qcelp -# audio/vnd.rhetorex.32kadpcm -audio/vnd.rip rip -# audio/vnd.sealedmedia.softseal.mpeg -# audio/vnd.vmx.cvsd -# audio/vorbis -# audio/vorbis-config -audio/webm weba -audio/x-aac aac -audio/x-aiff aif aiff aifc -audio/x-caf caf -audio/x-flac flac -audio/x-matroska mka -audio/x-mpegurl m3u -audio/x-ms-wax wax -audio/x-ms-wma wma -audio/x-pn-realaudio ram ra -audio/x-pn-realaudio-plugin rmp -# audio/x-tta -audio/x-wav wav -audio/xm xm -chemical/x-cdx cdx -chemical/x-cif cif -chemical/x-cmdf cmdf -chemical/x-cml cml -chemical/x-csml csml -# chemical/x-pdb -chemical/x-xyz xyz -image/bmp bmp -image/cgm cgm -# image/example -# image/fits -image/g3fax g3 -image/gif gif -image/ief ief -# image/jp2 -image/jpeg jpeg jpg jpe -# image/jpm -# image/jpx -image/ktx ktx -# image/naplps -image/png png -image/prs.btif btif -# image/prs.pti -image/sgi sgi -image/svg+xml svg svgz -# image/t38 -image/tiff tiff tif -# image/tiff-fx -image/vnd.adobe.photoshop psd -# image/vnd.cns.inf2 -image/vnd.dece.graphic uvi uvvi uvg uvvg -image/vnd.dvb.subtitle sub -image/vnd.djvu djvu djv -image/vnd.dwg dwg -image/vnd.dxf dxf -image/vnd.fastbidsheet fbs -image/vnd.fpx fpx -image/vnd.fst fst -image/vnd.fujixerox.edmics-mmr mmr -image/vnd.fujixerox.edmics-rlc rlc -# image/vnd.globalgraphics.pgb -# image/vnd.microsoft.icon -# image/vnd.mix -image/vnd.ms-modi mdi -image/vnd.ms-photo wdp -image/vnd.net-fpx npx -# image/vnd.radiance -# image/vnd.sealed.png -# image/vnd.sealedmedia.softseal.gif -# image/vnd.sealedmedia.softseal.jpg -# image/vnd.svf -image/vnd.wap.wbmp wbmp -image/vnd.xiff xif -image/webp webp -image/x-3ds 3ds -image/x-cmu-raster ras -image/x-cmx cmx -image/x-freehand fh fhc fh4 fh5 fh7 -image/x-icon ico -image/x-mrsid-image sid -image/x-pcx pcx -image/x-pict pic pct -image/x-portable-anymap pnm -image/x-portable-bitmap pbm -image/x-portable-graymap pgm -image/x-portable-pixmap ppm -image/x-rgb rgb -image/x-tga tga -image/x-xbitmap xbm -image/x-xpixmap xpm -image/x-xwindowdump xwd -# message/cpim -# message/delivery-status -# message/disposition-notification -# message/example -# message/external-body -# message/feedback-report -# message/global -# message/global-delivery-status -# message/global-disposition-notification -# message/global-headers -# message/http -# message/imdn+xml -# message/news -# message/partial -message/rfc822 eml mime -# message/s-http -# message/sip -# message/sipfrag -# message/tracking-status -# message/vnd.si.simp -# model/example -model/iges igs iges -model/mesh msh mesh silo -model/vnd.collada+xml dae -model/vnd.dwf dwf -# model/vnd.flatland.3dml -model/vnd.gdl gdl -# model/vnd.gs-gdl -# model/vnd.gs.gdl -model/vnd.gtw gtw -# model/vnd.moml+xml -model/vnd.mts mts -# model/vnd.parasolid.transmit.binary -# model/vnd.parasolid.transmit.text -model/vnd.vtu vtu -model/vrml wrl vrml -model/x3d+binary x3db x3dbz -model/x3d+vrml x3dv x3dvz -model/x3d+xml x3d x3dz -# multipart/alternative -# multipart/appledouble -# multipart/byteranges -# multipart/digest -# multipart/encrypted -# multipart/example -# multipart/form-data -# multipart/header-set -# multipart/mixed -# multipart/parallel -# multipart/related -# multipart/report -# multipart/signed -# multipart/voice-message -# text/1d-interleaved-parityfec -text/cache-manifest appcache -text/calendar ics ifb -text/css css -text/csv csv -# text/directory -# text/dns -# text/ecmascript -# text/enriched -# text/example -# text/fwdred -text/html html htm -# text/javascript -text/n3 n3 -# text/parityfec -text/plain txt text conf def list log in -# text/prs.fallenstein.rst -text/prs.lines.tag dsc -# text/vnd.radisys.msml-basic-layout -# text/red -# text/rfc822-headers -text/richtext rtx -# text/rtf -# text/rtp-enc-aescm128 -# text/rtx -text/sgml sgml sgm -# text/t140 -text/tab-separated-values tsv -text/troff t tr roff man me ms -text/turtle ttl -# text/ulpfec -text/uri-list uri uris urls -text/vcard vcard -# text/vnd.abc -text/vnd.curl curl -text/vnd.curl.dcurl dcurl -text/vnd.curl.scurl scurl -text/vnd.curl.mcurl mcurl -# text/vnd.dmclientscript -text/vnd.dvb.subtitle sub -# text/vnd.esmertec.theme-descriptor -text/vnd.fly fly -text/vnd.fmi.flexstor flx -text/vnd.graphviz gv -text/vnd.in3d.3dml 3dml -text/vnd.in3d.spot spot -# text/vnd.iptc.newsml -# text/vnd.iptc.nitf -# text/vnd.latex-z -# text/vnd.motorola.reflex -# text/vnd.ms-mediapackage -# text/vnd.net2phone.commcenter.command -# text/vnd.si.uricatalogue -text/vnd.sun.j2me.app-descriptor jad -# text/vnd.trolltech.linguist -# text/vnd.wap.si -# text/vnd.wap.sl -text/vnd.wap.wml wml -text/vnd.wap.wmlscript wmls -text/x-asm s asm -text/x-c c cc cxx cpp h hh dic -text/x-fortran f for f77 f90 -text/x-java-source java -text/x-opml opml -text/x-pascal p pas -text/x-nfo nfo -text/x-setext etx -text/x-sfv sfv -text/x-uuencode uu -text/x-vcalendar vcs -text/x-vcard vcf -# text/xml -# text/xml-external-parsed-entity -# video/1d-interleaved-parityfec -video/3gpp 3gp -# video/3gpp-tt -video/3gpp2 3g2 -# video/bmpeg -# video/bt656 -# video/celb -# video/dv -# video/example -video/h261 h261 -video/h263 h263 -# video/h263-1998 -# video/h263-2000 -video/h264 h264 -# video/h264-rcdo -# video/h264-svc -video/jpeg jpgv -# video/jpeg2000 -video/jpm jpm jpgm -video/mj2 mj2 mjp2 -# video/mp1s -# video/mp2p -# video/mp2t -video/mp4 mp4 mp4v mpg4 -# video/mp4v-es -video/mpeg mpeg mpg mpe m1v m2v -# video/mpeg4-generic -# video/mpv -# video/nv -video/ogg ogv -# video/parityfec -# video/pointer -video/quicktime qt mov -# video/raw -# video/rtp-enc-aescm128 -# video/rtx -# video/smpte292m -# video/ulpfec -# video/vc1 -# video/vnd.cctv -video/vnd.dece.hd uvh uvvh -video/vnd.dece.mobile uvm uvvm -# video/vnd.dece.mp4 -video/vnd.dece.pd uvp uvvp -video/vnd.dece.sd uvs uvvs -video/vnd.dece.video uvv uvvv -# video/vnd.directv.mpeg -# video/vnd.directv.mpeg-tts -# video/vnd.dlna.mpeg-tts -video/vnd.dvb.file dvb -video/vnd.fvt fvt -# video/vnd.hns.video -# video/vnd.iptvforum.1dparityfec-1010 -# video/vnd.iptvforum.1dparityfec-2005 -# video/vnd.iptvforum.2dparityfec-1010 -# video/vnd.iptvforum.2dparityfec-2005 -# video/vnd.iptvforum.ttsavc -# video/vnd.iptvforum.ttsmpeg2 -# video/vnd.motorola.video -# video/vnd.motorola.videop -video/vnd.mpegurl mxu m4u -video/vnd.ms-playready.media.pyv pyv -# video/vnd.nokia.interleaved-multimedia -# video/vnd.nokia.videovoip -# video/vnd.objectvideo -# video/vnd.sealed.mpeg1 -# video/vnd.sealed.mpeg4 -# video/vnd.sealed.swf -# video/vnd.sealedmedia.softseal.mov -video/vnd.uvvu.mp4 uvu uvvu -video/vnd.vivo viv -video/webm webm -video/x-f4v f4v -video/x-fli fli -video/x-flv flv -video/x-m4v m4v -video/x-matroska mkv mk3d mks -video/x-mng mng -video/x-ms-asf asf asx -video/x-ms-vob vob -video/x-ms-wm wm -video/x-ms-wmv wmv -video/x-ms-wmx wmx -video/x-ms-wvx wvx -video/x-msvideo avi -video/x-sgi-movie movie -video/x-smv smv -x-conference/x-cooltalk ice diff --git a/websvr/lib/types/node.types b/websvr/lib/types/node.types deleted file mode 100644 index 988d848..0000000 --- a/websvr/lib/types/node.types +++ /dev/null @@ -1,71 +0,0 @@ -# What: WebVTT -# Why: To allow formats intended for marking up external text track resources. -# http://dev.w3.org/html5/webvtt/ -# Added by: niftylettuce -text/vtt vtt - -# What: Google Chrome Extension -# Why: To allow apps to (work) be served with the right content type header. -# http://codereview.chromium.org/2830017 -# Added by: niftylettuce -application/x-chrome-extension crx - -# What: HTC support -# Why: To properly render .htc files such as CSS3PIE -# Added by: niftylettuce -text/x-component htc - -# What: HTML5 application cache manifest -# Why: De-facto standard. Required by Mozilla browser when serving HTML5 apps -# per https://developer.mozilla.org/en/offline_resources_in_firefox -# Added by: louisremi -text/cache-manifest appcache manifest - -# What: node binary buffer format -# Why: semi-standard extension w/in the node community -# Added by: tootallnate -application/octet-stream buffer - -# What: The "protected" MP-4 formats used by iTunes. -# Why: Required for streaming music to browsers (?) -# Added by: broofa -application/mp4 m4p -audio/mp4 m4a - -# What: Video format, Part of RFC1890 -# Why: See https://github.com/bentomas/node-mime/pull/6 -# Added by: mjrusso -video/MP2T ts - -# What: EventSource mime type -# Why: mime type of Server-Sent Events stream -# http://www.w3.org/TR/eventsource/#text-event-stream -# Added by: francois2metz -text/event-stream event-stream - -# What: Mozilla App manifest mime type -# Why: https://developer.mozilla.org/en/Apps/Manifest#Serving_manifests -# Added by: ednapiranha -application/x-web-app-manifest+json webapp - -# What: Lua file types -# Why: Googling around shows de-facto consensus on these -# Added by: creationix (Issue #45) -text/x-lua lua -application/x-lua-bytecode luac - -# What: Markdown files, as per http://daringfireball.net/projects/markdown/syntax -# Why: http://stackoverflow.com/questions/10701983/what-is-the-mime-type-for-markdown -# Added by: avoidwork -text/x-markdown markdown md mkd - - -# What: ini files -# Why: because they're just text files -# Added by: Matthew Kastor -text/plain ini - -# What: DASH Adaptive Streaming manifest -# Why: https://developer.mozilla.org/en-US/docs/DASH_Adaptive_Streaming_for_HTML_5_Video -# Added by: eelcocramer -application/dash+xml mdp From 29d534fab062a86d1a5f6341be181db0ce18f0dd Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 8 May 2013 10:19:07 +0800 Subject: [PATCH 071/195] version: 1.0.1 update demo sites. --- README.md | 13 ++++++++++--- package.json | 2 +- website/svr/sitetest.js | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0ee6329..49da680 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ WebSvr ============== -A simple web server based on node.js. +A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session. + Lincenses: MIT Features @@ -12,12 +13,18 @@ Features - File: Support uploading files - Custom index pages +Install +-------------- + + npm install websvr + + Start -------------- It's simple to start the websvr. - //import WebSvr module, assume we don't have NPM. - var WebSvr = require("./../../websvr/websvr.js"); + //import WebSvr module + var WebSvr = require("websvr"); //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 diff --git a/package.json b/package.json index 99a69e3..f0c6609 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.0", + "version": "0.1.1", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 0ade6ae..34e2e61 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -1,5 +1,5 @@ -//import WebSvr module, assume we don't have NPM. -var WebSvr = require("./../../websvr/websvr.js"); +//import WebSvr module +var WebSvr = require("websvr"); //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 From 1834889d832f5be8f850731a97761f68afd1b7ae Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 15 May 2013 16:49:01 +0800 Subject: [PATCH 072/195] WebSvr: Add websvr.handler & websvr.get & websvr.handle API. --- websvr/websvr.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/websvr/websvr.js b/websvr/websvr.js index 2cb921e..f14ce84 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1022,7 +1022,10 @@ var WebSvr = module.exports = function(options) { self.file = Filter.file; //Handler + self.get = Handler.url; self.url = Handler.url; + self.handle = Handler.url; + self.handler = Handler.url; self.post = Handler.post; self.session = Handler.session; From 42b5a46cfbd0605db8089aa182ea16c6bf0b1fa0 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 15 May 2013 16:51:12 +0800 Subject: [PATCH 073/195] WebSvr: Update comments. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 49da680..91bbded 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,8 @@ Template -------------- Render template with params, using doT template engine - webSvr.url("template.node", function(req, res) { + //webSvr.handle equal to webSvr.get/ webSvr.url + webSvr.handle("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //render template with session: { "username" : "admin" } req.session.get(function(session) { From 9da0554af20d1c5369e7437faa1d04cf9c89dc3f Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 22 May 2013 10:06:24 +0800 Subject: [PATCH 074/195] Fixed bug: Ignore the header if already setted. --- package.json | 2 +- websvr/websvr.js | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f0c6609..e58f03c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.1", + "version": "0.1.2", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index f14ce84..d7d98e0 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1,16 +1,8 @@ /* -* Description: node-websvr +* Description: websvr * Author: Kris Zhang * Licenses: MIT * Project url: https://github.com/newghost/node-websvr -* -* Referenced projects: -* Formidable: Support uploading files, integrate - https://github.com/felixge/node-formidable/ -* MIME: content-type in header - https://github.com/broofa/node-mime -* template: Template Engine - https://github.com/olado/doT */ //Node libraries @@ -1010,7 +1002,10 @@ var WebSvr = module.exports = function(options) { Logger.debug(err); return; } - res.setHeader("Content-Type", mime.lookup(fullPath)); + + !res.getHeader("Content-Type") + && res.setHeader("Content-Type", mime.lookup(fullPath)); + res.writeHead(200); res.end(data, "binary"); }); From d2a36abda419730ecde0bad1429569608297f5d9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 23 May 2013 09:34:55 +0800 Subject: [PATCH 075/195] Update version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e58f03c..7b6f091 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.2", + "version": "0.1.3", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From bdb02d8088d960e358ac61f12264464c1953a6ce Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 28 May 2013 15:29:10 +0800 Subject: [PATCH 076/195] Add post data type support, etc: --- websvr/websvr.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index d7d98e0..c3de9bf 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -330,10 +330,18 @@ var WebSvr = module.exports = function(options) { */ var parseBody = function() { //need to parse the request? - if (mapper.parse && typeof req.body == "undefined") { + if ((mapper.parse || mapper.post) && typeof req.body == "undefined") { //Must parser the request first, or the post data will lost; BodyParser(req, res, function(data) { - req.body = data; + var body = data; + + mapper.post == "json" + && (body = JSON.parse(data || "{}")); + + mapper.post == "qs" || mapper.post == "querystring" + && (body = qs.parse(data || "")); + + req.body = body; parseSession(); }); }else{ From 628338ee88d90529a4e36c9789f450a21a3a2251 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 29 May 2013 15:52:09 +0800 Subject: [PATCH 077/195] Update version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b6f091..05ed657 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.3", + "version": "0.1.4", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 14c5f0a59860595a250c7d459c0eeaf76797cad4 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 6 Jun 2013 16:30:08 +0800 Subject: [PATCH 078/195] Update README.md --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 91bbded..0304cac 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,14 @@ Advanced useage: All the request under "test/" will parse the post data and sess console.log("session", val); !val && res.end("You must login, first!"); + + //Link to next filter + req.filter.next(); }); + } else { + //Link to next filter + req.filter.next(); } - - //Link to next filter - req.filter.next(); }); @@ -209,4 +212,4 @@ Do you want to re-use the filters & handlers? node-websvr ==== 基äºNodeJS的一个æç®€WebæœåС噍, 专为ARM设计。 -å‡è®¾åµŒå…¥å¼è®¾å¤‡éœ€è¦ä¿æŒé•¿æ—¶é—´ç¨³å®šè¿è¡Œï¼Œå½“é‡åˆ°é—®é¢˜æ—¶ä¹Ÿå¯è‡ªåЍé‡å¯å¹¶æ¢å¤æ­¤å‰ç”¨æˆ·çš„Session会è¯ã€‚ \ No newline at end of file +å‡è®¾åµŒå…¥å¼è®¾å¤‡éœ€è¦ä¿æŒé•¿æ—¶é—´ç¨³å®šè¿è¡Œï¼Œå½“é‡åˆ°é—®é¢˜æ—¶ä¹Ÿå¯è‡ªåЍé‡å¯å¹¶æ¢å¤æ­¤å‰ç”¨æˆ·çš„Session会è¯ã€‚ From bcc8a9f345613ab0d18d7bd79a4f840055e06fcb Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 7 Jun 2013 09:39:44 +0800 Subject: [PATCH 079/195] Update README.md --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0304cac..c5998bb 100644 --- a/README.md +++ b/README.md @@ -185,15 +185,9 @@ Do you want to re-use the filters & handlers? - - - - - - - - - +Demos +==== +[icalc](https://github.com/newghost/websvr-icalc/) @@ -212,4 +206,4 @@ Do you want to re-use the filters & handlers? node-websvr ==== 基äºNodeJS的一个æç®€WebæœåС噍, 专为ARM设计。 -å‡è®¾åµŒå…¥å¼è®¾å¤‡éœ€è¦ä¿æŒé•¿æ—¶é—´ç¨³å®šè¿è¡Œï¼Œå½“é‡åˆ°é—®é¢˜æ—¶ä¹Ÿå¯è‡ªåЍé‡å¯å¹¶æ¢å¤æ­¤å‰ç”¨æˆ·çš„Session会è¯ã€‚ +å‡è®¾åµŒå…¥å¼è®¾å¤‡éœ€è¦ä¿æŒé•¿æ—¶é—´ç¨³å®šè¿è¡Œï¼Œå½“é‡åˆ°é—®é¢˜æ—¶ä¹Ÿå¯è‡ªåЍé‡å¯å¹¶æ¢å¤æ­¤å‰ç”¨æˆ·çš„Session会è¯ã€‚ From 184909f1533b0bb26edf101fc46848a1307f9574 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 25 Jun 2013 16:41:52 +0800 Subject: [PATCH 080/195] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c5998bb..eadd1b0 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ WebSvr ============== -A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session. +A simple web server, implement HttpModule(filter) and HttpHandler(servlet), autorecover user session when run into problems. Lincenses: MIT Features -------------- -- Designed for ARM: We need to keep embed device running stable in a very long time(> 1 year?), it may run into issues but it can restart and re-covery the user sessions automatically. -- Filter: A request will try to match all the filters first, and then pass to the Handler -- Handler: When a request matched a handler, it will returned, only one handler will be executed +- Auto recover: It may run into problems but it can restart and re-covery the user sessions automatically. +- Filter (Middleware): A request will try to match all the filters first. +- Handler: When a request matched a handler, it will returned, only one handler will be executed. - Session: Stored in file, with JSON format - File: Support uploading files -- Custom index pages +- Cache: Client-cahce is supported. Install -------------- @@ -35,7 +35,7 @@ It's simple to start the websvr. }).start(); -Filter +Filter (HttpModule) -------------- Session based authentication, basically useage: @@ -75,7 +75,7 @@ Advanced useage: All the request under "test/" will parse the post data and sess }); -Handler +Handler (HttpHandler, Servlet) -------------- Handle Login and put the username in Session From 58d54997797ed7669d7336d0c997dbce3d823d5e Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 25 Jun 2013 17:15:51 +0800 Subject: [PATCH 081/195] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eadd1b0..3f1a5a3 100644 --- a/README.md +++ b/README.md @@ -185,9 +185,10 @@ Do you want to re-use the filters & handlers? -Demos -==== -[icalc](https://github.com/newghost/websvr-icalc/) +Demo Sites +---- + +1. icalc: url [icalc.cn](http://icalc.cn), source code [github](https://github.com/newghost/websvr-icalc/) From 114a1a6ddfc2f2947510b8f35035bfa25cdb34dd Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 30 Aug 2013 16:41:31 +0800 Subject: [PATCH 082/195] Update API: Remove "querystring", only support { post: "qs" } --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index c3de9bf..878c38f 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -338,7 +338,7 @@ var WebSvr = module.exports = function(options) { mapper.post == "json" && (body = JSON.parse(data || "{}")); - mapper.post == "qs" || mapper.post == "querystring" + mapper.post == "qs" && (body = qs.parse(data || "")); req.body = body; From da48a04f4cd9602a67533cfa4b75591bd9454313 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 30 Aug 2013 17:06:39 +0800 Subject: [PATCH 083/195] Export settings on websvr instance; --- websvr/websvr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/websvr/websvr.js b/websvr/websvr.js index 878c38f..71683e9 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1031,6 +1031,7 @@ var WebSvr = module.exports = function(options) { self.handler = Handler.url; self.post = Handler.post; self.session = Handler.session; + self.settings = Settings; //Get a full path of a request self.getFullPath = function(filePath) { From bbf3292098faba15802a72bef06d828a0b0a7d08 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 30 Aug 2013 22:01:03 +0800 Subject: [PATCH 084/195] Update Template: optimize: res.render add: webSvr.engine --- README.md | 109 ++++++++++++++++++++++++++++++++++++++++------- websvr/websvr.js | 53 +++++++++++------------ 2 files changed, 117 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 3f1a5a3..3d3f446 100644 --- a/README.md +++ b/README.md @@ -120,18 +120,103 @@ Receive upload file (it's a specfic filter) res.end(JSON.stringify(req.files)); }); + Template -------------- Render template with params, using doT template engine - //webSvr.handle equal to webSvr.get/ webSvr.url - webSvr.handle("template.node", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //render template with session: { "username" : "admin" } - req.session.get(function(session) { - res.render(req, session); - }); - }); + res.render([tmplPath, ] model); + +"tmplPath" is optional, it will get the tmplate from req.url + + res.render({json: true}); + +View could be a relative template path, relative to root web dir + + res.render("list.tmpl", {json: true}); + +You can change template engine, etc: webSvr.engine(require("doT")); + + webSvr.engine(engineLib); + + + +Settings +-------------- +Return configuration of current WebSvr instance + + webSvr.settings + +Settings API: + + var Settings = { + //root folder of web + root: "../" + + //http start + //default port of http + , port: 8054 + + //default port of https + , httpsPort: 8443 + , httpsKey: "" + , httpsCert: "" + + //list files in directory + , listDir: false + //enable client-side cache(304) + , cache: true + //enable debug information output + , debug: true + //receive buffer, default size 32k, etc: receive post data from ajax request + , bufferSize: 32768 + + //default pages, only one is supported + , defaultPage: "index.html" + + //logger file path + , logger: os.tmpDir() + "/log.txt" + + /* + Session timeout, in milliseconds. + When session is expired, session file will not deleted. + */ + , sessionTimeout: 1440000 + /* + Session garbage collection time, in milliseconds. + When session expired time is more than (sessionAge + sessionGCT), + then session file will be unlinked. + */ + , sessionGarbage: 3460000 + + //session file stored here + , sessionDir: os.tmpDir() + + //tempary upload file stored here + , uploadDir: os.tmpDir() + }; + +Response +-------------- +Extension on reponse object + +Ouput file, relative path, relative to the web root + + res.writeFile(filePath, [callback]); + +Ouput file, absolute path, relative to the server running + + res.sendFile(filePath, [callback]); + +Reidrect request + + res.redirect(url); + +Return request object + + res.req + + Other APIs -------------- @@ -167,14 +252,6 @@ Start a https server, make sure that the port will no conflict with others. , httpsKey: require("fs").readFileSync("svr/cert/privatekey.pem") , httpsCert: require("fs").readFileSync("svr/cert/certificate.pem") - //, defaultPage: "index.htm" - , listDir: true - - //Change the default locations of tmp session and upload files - //session file stored here - , sessionDir: "tmp/session/" - //tempary upload file stored here - , uploadDir: "tmp/upload/" }).start(); Do you want to re-use the filters & handlers? diff --git a/websvr/websvr.js b/websvr/websvr.js index 71683e9..3a55efe 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -808,7 +808,7 @@ var WebSvr = module.exports = function(options) { }()); /* - * Templates + * Template Engine */ var Template = (function() { @@ -836,36 +836,25 @@ var WebSvr = module.exports = function(options) { }; return { - //render templates - render: function(chrunk, params) { - var res = this, - end = res.end; - - var url = chrunk.url, - con = chrunk.constructor; - - //It's a http request (it has "url") - if (url) { - getFile(url, function(tmpl) { - render(tmpl, params, end); - }); - - //It's html contents (template codes) - } else if (con == String) { - render(chrunk, params, end); - - //It's Array object (template file path) - } else if (con == Array) { - getFile(chrunk[0], function(tmpl) { - render(tmpl, params, end); - }); + //render templates + render: function(tmplUrl, model) { + var res = this, + end = res.end; - //Nothing matched end the response - } else { - end(); - } + if (arguments.length == 1) { + model = tmplUrl; + tmplUrl = res.req.url; - } + tmplUrl.indexOf('?') > -1 && (tmplUrl = tmplUrl.substr(0, tmplUrl.indexOf('?'))); + } + + getFile(tmplUrl, function(tmpl) { + render(tmpl, model, end); + }); + } + , engine: function(engineLib) { + engine = engineLib; + } } }()); @@ -958,6 +947,9 @@ var WebSvr = module.exports = function(options) { }; var requestHandler = function(req, res) { + //Make request accessible in response object + res.req = req; + //Response may be shutdown when do the filter, in order not to cause exception, //Rewrite the write/writeHead functionalities of current response object var endFn = res.end; @@ -1033,6 +1025,9 @@ var WebSvr = module.exports = function(options) { self.session = Handler.session; self.settings = Settings; + //Template + self.engine = Template.engine; + //Get a full path of a request self.getFullPath = function(filePath) { return path.join(root, filePath); From 4670ba9cc52729ba0cc99179d40baa18feba3998 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 31 Aug 2013 08:39:47 +0800 Subject: [PATCH 085/195] Update README --- README.md | 84 ++++++++++++++++++++++++++----------------------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 3d3f446..c848a6b 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,6 @@ Session based authentication, basically useage: session: init the session and stored in req.session; */ webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); - //Link to next filter req.filter.next(); }, {parse:true, session:true}); @@ -84,41 +81,18 @@ Handle Login and put the username in Session username: admin password: 12345678 */ - webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); + webSvr.handle("login.do", function(req, res) { + var qs = req.body; if (qs.username == "admin" && qs.password == "12345678") { //Put key/value pair in session - //TODO: Support put JSON object directly req.session.set("username", qs.username, function(session) { - //res.writeHead(200, {"Content-Type": "text/html"}); - //res.writeFile("/web/setting.htm"); - //TODO: Error handler of undefined methods - console.log(session); res.redirect("/web/setting.htm"); }); }else{ res.writeHead(401); res.end("Wrong username/password"); } - }); - -File --------------- -Receive upload file (it's a specfic filter) - - /* - Uploader: upload.do => (receive handler) - */ - webSvr.file("upload.do", function(req, res) { - res.writeHead(200, {"Content-Type": "text/plain"}); - //Upload file is stored in req.files - //form fields is stored in req.body - res.write(JSON.stringify(req.body)); - res.end(JSON.stringify(req.files)); - }); + }, {parse: "qs"}); Template @@ -218,23 +192,45 @@ Return request object -Other APIs +WebSvr APIs -------------- -Redirect, Url Mapping, Post Data - - webSvr - //Mapping "sitest" to tool/Combine.js, trying at: http://localhost:8054/combine - .url("sitetest", ["svr/sitetest.js"]) - //Mapping "hello" to a string, trying at http://localhost:8054/hello - .url("hello", "Hello WebSvr!") - //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm - .post("post.htm", function(req, res) { - res.writeHead(200, {"Content-Type": "text/html"}); - //With session support: "{session: true}" - res.write("You username is " + req.session.get("username")); - res.write('

    '); +Mapping url to file + + webSvr.url("sitetest", ["svr/sitetest.js"]); + +Mapping url to string + + webSvr.url("hello", "Hello WebSvr!") + +Handle post + + webSvr.post("post.htm", function(req, res) { res.end('Received : ' + req.body); - }, {session: true}); + }); + + //Equal to + webSvr.handle("sessionrequire", function(req, res) { + console.log(req.session); + res.end(); + }, {parse: true}); + +Handle session + + webSvr.session("sessionrequire", function(req, res) { + console.log(req.session); + res.end(); + }); + + +Handle upload file, it's a specfic filter + + webSvr.file("upload.do", function(req, res) { + res.writeHead(200, {"Content-Type": "text/plain"}); + //Upload file is stored in req.files + //form fields is stored in req.body + res.write(JSON.stringify(req.body)); + res.end(JSON.stringify(req.files)); + }); Multi-instance support From a69f7e842762a731d0cefece6e308b7dbcbd6c0d Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 31 Aug 2013 08:42:37 +0800 Subject: [PATCH 086/195] Update README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c848a6b..755bcc5 100644 --- a/README.md +++ b/README.md @@ -209,9 +209,8 @@ Handle post }); //Equal to - webSvr.handle("sessionrequire", function(req, res) { - console.log(req.session); - res.end(); + webSvr.handle("post.htm", function(req, res) { + res.end('Received : ' + req.body); }, {parse: true}); Handle session From 7d295ab0c53baed3f30532eebfa2b3ab09b00a85 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 31 Aug 2013 08:45:55 +0800 Subject: [PATCH 087/195] Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 755bcc5..f7af53e 100644 --- a/README.md +++ b/README.md @@ -99,13 +99,13 @@ Template -------------- Render template with params, using doT template engine - res.render([tmplPath, ] model); + res.render([view, ] model); -"tmplPath" is optional, it will get the tmplate from req.url +View is optional, in this case it will get the template path from req.url res.render({json: true}); -View could be a relative template path, relative to root web dir +View is a relative path, relative to root web dir res.render("list.tmpl", {json: true}); From 94b2c3eb48c4ac2e4a8ff8dbc92fe6b9d33305e6 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 31 Aug 2013 12:05:09 +0800 Subject: [PATCH 088/195] Add buffer size; --- README.md | 8 ++++++-- websvr/websvr.js | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7af53e..8a784df 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Handle Login and put the username in Session res.writeHead(401); res.end("Wrong username/password"); } - }, {parse: "qs"}); + }, {post: "qs"}); Template @@ -211,7 +211,11 @@ Handle post //Equal to webSvr.handle("post.htm", function(req, res) { res.end('Received : ' + req.body); - }, {parse: true}); + }, {post: true}); + +Post type + + post: true/"json"/"qs" Handle session diff --git a/websvr/websvr.js b/websvr/websvr.js index 3a55efe..4e63bb8 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -68,8 +68,8 @@ var WebSvr = module.exports = function(options) { , cache: true //enable debug information output , debug: true - //receive buffer, default size 32k, etc: receive post data from ajax request - , bufferSize: 32768 + //receive buffer, default size 255Kb, etc: receive post data from ajax request + , bufferSize: 261120 //default pages, only one is supported , defaultPage: "index.html" From d17f8bd7fb0664ea247a876fbc6825fae707eac6 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 31 Aug 2013 12:47:10 +0800 Subject: [PATCH 089/195] Upgrade npm version; --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05ed657..e81ede5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.4", + "version": "0.1.5", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From d5e94020f961a5caabc2920120213e48b5342c28 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 1 Sep 2013 19:35:21 +0800 Subject: [PATCH 090/195] Add LICENSE file --- LICENSE | 22 ++++++++++++++++++++++ README.md | 15 +++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..03cffcc --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) Kris Zhang + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 8a784df..55ba767 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ WebSvr ============== A simple web server, implement HttpModule(filter) and HttpHandler(servlet), autorecover user session when run into problems. -Lincenses: MIT Features -------------- @@ -260,13 +259,9 @@ Do you want to re-use the filters & handlers? - -Demo Sites +Lincenses ---- - -1. icalc: url [icalc.cn](http://icalc.cn), source code [github](https://github.com/newghost/websvr-icalc/) - - +MIT, see our license file @@ -278,9 +273,13 @@ Demo Sites +Demo Sites +---- +1. ourjs: url [ourjs.com](http://ourjs.com) +2. icalc: url [icalc.cn](http://icalc.cn), source code [github](https://github.com/newghost/websvr-icalc/) -node-websvr +Websvr ==== 基äºNodeJS的一个æç®€WebæœåС噍, 专为ARM设计。 å‡è®¾åµŒå…¥å¼è®¾å¤‡éœ€è¦ä¿æŒé•¿æ—¶é—´ç¨³å®šè¿è¡Œï¼Œå½“é‡åˆ°é—®é¢˜æ—¶ä¹Ÿå¯è‡ªåЍé‡å¯å¹¶æ¢å¤æ­¤å‰ç”¨æˆ·çš„Session会è¯ã€‚ From b639f91d2efd3aa77034418029678a977ae50249 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 5 Sep 2013 15:05:53 +0800 Subject: [PATCH 091/195] fix output error message; --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 4e63bb8..e5a2bd3 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -831,7 +831,7 @@ var WebSvr = module.exports = function(options) { outFn(tmplFn(params)); } catch(err) { Logger.debug(err); - outFn(err); + outFn(JSON.stringify(err)); } }; From f726fe82d0bee5c0eb206e963009708038423851 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 10 Sep 2013 22:31:22 +0800 Subject: [PATCH 092/195] Update handle match rule; --- websvr/websvr.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 4e63bb8..6c98fd6 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -539,7 +539,10 @@ var WebSvr = module.exports = function(options) { if (!expression) return true; switch (expression.constructor) { - case String: return req.url.indexOf(expression) > -1; + //String handler must start with root path, but it can bypass '/' + case String: + var idx = req.url.indexOf(expression); + return idx == 0 || idx == 1; case RegExp: return expression.test(req.url); } From 70a0dfca8f478448a8a01adf889fe684969dd5eb Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 11 Sep 2013 09:14:42 +0800 Subject: [PATCH 093/195] Update filter and handler match rules; --- README.md | 23 +++++++++++++++++++++++ websvr/websvr.js | 10 +++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 55ba767..10bf894 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,29 @@ Handle Login and put the username in Session }, {post: "qs"}); +Note: +-------------- +Filter and Handler doesn't have the same match rules when you sending a request + +Filter : Match any section in the request url, etc: + + websvr.filter(".svr", cb); + +The result is + + request: "domain.com/admin/root/login.svr" match: true + +Handler : Match from the begining but it can bypass '/', etc: + + websvr.handle("root/login", cb) //or + websvr.handle("/root/login", cb) + +The result is: + + request: "domain.com/root/login.svr" match: true + request: "domain.com/admin/root/login.svr" match: false + + Template -------------- Render template with params, using doT template engine diff --git a/websvr/websvr.js b/websvr/websvr.js index 4fc7e12..69fa44e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -530,8 +530,11 @@ var WebSvr = module.exports = function(options) { Mapper.prototype = { /* Does this mapper matched this request? + Filter and Handler doesn't have the same matched rules when you passing a string + Filter : Match any section of the request url, etc: websvr.filter(".svr", cb); + Handler : Match from the begining but it can bypass '/', etc: websvr.handle("root/login", cb) or websvr.handle("/root/login") */ - match: function(req) { + match: function(req, isHandler) { var self = this, expression = self.expression; @@ -542,7 +545,7 @@ var WebSvr = module.exports = function(options) { //String handler must start with root path, but it can bypass '/' case String: var idx = req.url.indexOf(expression); - return idx == 0 || idx == 1; + return isHandler ? (idx == 0 || idx == 1) : (idx > -1); case RegExp: return expression.test(req.url); } @@ -683,7 +686,8 @@ var WebSvr = module.exports = function(options) { for(var i = 0, len = Handler.handlers.length; i < len ; i++) { var mapper = Handler.handlers[i]; - if (mapper.match(req)) { + //This is handler match + if (mapper.match(req, true)) { Logger.debug("handler matched", i, mapper.expression, req.url); From 4324ac1aafeb460126d34b231e146909c66f0eaf Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 11 Sep 2013 09:15:10 +0800 Subject: [PATCH 094/195] Update version; --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e81ede5..4ecc577 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.5", + "version": "0.1.6", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 4f15bd373f23a972af789b32bf139ca42026021a Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 27 Sep 2013 13:25:51 +0800 Subject: [PATCH 095/195] Add Array support in matching expressions of Filter & Handler --- README.md | 21 ++++++++++++++------- package.json | 2 +- websvr/websvr.js | 20 ++++++++++++++------ 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 10bf894..42b5275 100644 --- a/README.md +++ b/README.md @@ -100,22 +100,23 @@ Filter and Handler doesn't have the same match rules when you sending a request Filter : Match any section in the request url, etc: - websvr.filter(".svr", cb); - -The result is - + websvr.filter(".svr", cb); + +The result is + request: "domain.com/admin/root/login.svr" match: true Handler : Match from the begining but it can bypass '/', etc: websvr.handle("root/login", cb) //or - websvr.handle("/root/login", cb) - -The result is: + websvr.handle("/root/login", cb) + +The result is: request: "domain.com/root/login.svr" match: true request: "domain.com/admin/root/login.svr" match: false +You can use regular expression to match part of url in Handler. Template -------------- @@ -257,6 +258,12 @@ Handle upload file, it's a specfic filter res.end(JSON.stringify(req.files)); }); +Multi-Mapping in Handler or Filter + + webSvr.handle(["about", "help", "welcome"], function(req, res) { + res.writeFile(req.url + ".shtml"); + }, {post: true}); + Multi-instance support -------------- diff --git a/package.json b/package.json index 4ecc577..df75ead 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.6", + "version": "0.1.7", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 69fa44e..62f36a1 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -547,6 +547,14 @@ var WebSvr = module.exports = function(options) { var idx = req.url.indexOf(expression); return isHandler ? (idx == 0 || idx == 1) : (idx > -1); case RegExp: return expression.test(req.url); + case Array: + for (var i = 0, l = expression.length; i < l; i++) { + var idx = req.url.indexOf(expression[i]) + , tag = isHandler ? (idx == 0 || idx == 1) : (idx > -1); + if (tag) return true; + } + return false; + } return false; @@ -822,17 +830,17 @@ var WebSvr = module.exports = function(options) { var engine = require("dot"); //get a file - var getFile = function(filename, cb){ + var getFile = function(filename, cb) { var fullpath = path.join(Settings.root, filename); - fs.readFile(fullpath, function (err, html) { + fs.readFile(fullpath, function(err, html) { err && Logger.debug(err); err ? cb("") : cb(html); }); }; //render a file - var render = function(chrunk, params, outFn){ + var render = function(chrunk, params, outFn) { try { tmplFn = engine.compile(chrunk, params); outFn(tmplFn(params)); @@ -931,7 +939,7 @@ var WebSvr = module.exports = function(options) { if (defaultPage) { var defaultPath = path.join(dirPath, defaultPage); - fs.exists(defaultPath, function (exists) { + fs.exists(defaultPath, function(exists) { //If page exists hanle it again if (exists) { //In order to make it as a dir path for loading static resources @@ -989,7 +997,7 @@ var WebSvr = module.exports = function(options) { res.render = Template.render; //initial httprequest - var filterChain = new FilterChain(function(){ + var filterChain = new FilterChain(function() { //if handler not match, send the request !Handler.handle(req, res) && fileHandler(req, res); @@ -1151,4 +1159,4 @@ var WebSvr = module.exports = function(options) { return self; -}; +}; \ No newline at end of file From 1b3c6edfde6b36b0574e99b4fd6eaeabe1052a2d Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 26 Oct 2013 22:44:45 +0800 Subject: [PATCH 096/195] Add exception handler when parse JSON or QueryString; --- package.json | 2 +- websvr/websvr.js | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index df75ead..1ede88b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.7", + "version": "0.1.8", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 62f36a1..d911195 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -335,11 +335,16 @@ var WebSvr = module.exports = function(options) { BodyParser(req, res, function(data) { var body = data; - mapper.post == "json" - && (body = JSON.parse(data || "{}")); - - mapper.post == "qs" - && (body = qs.parse(data || "")); + //handle exception + try { + mapper.post == "json" + && (body = JSON.parse(data || "{}")); + + mapper.post == "qs" + && (body = qs.parse(data || "")); + } catch(e) { + body = {}; + } req.body = body; parseSession(); From 5c68e2c9b9f1e73a4f94a1fe2818f363c2d901d7 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 27 Oct 2013 09:46:16 +0800 Subject: [PATCH 097/195] Update template engine interface; --- README.md | 9 +++++++-- website/svr/sitetest.js | 26 ++++++++++++++++++++++---- website/web/{index.htm => login.htm} | 0 website/web/template.jade | 16 ++++++++++++++++ websvr/websvr.js | 9 +++++---- 5 files changed, 50 insertions(+), 10 deletions(-) rename website/web/{index.htm => login.htm} (100%) create mode 100644 website/web/template.jade diff --git a/README.md b/README.md index 42b5275..8bb5441 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,14 @@ View is a relative path, relative to root web dir res.render("list.tmpl", {json: true}); -You can change template engine, etc: webSvr.engine(require("doT")); +You can change template engine, - webSvr.engine(engineLib); + webSvr.engine(engineFunc); + +etc: + + webSvr.engine(require("doT").compile); + webSvr.engine(require("jade").compile); diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 34e2e61..742a65f 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -4,7 +4,7 @@ var WebSvr = require("websvr"); //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 var webSvr = new WebSvr({ - root: "./" + root: "./web" , listDir: true , debug: true }).start(); @@ -63,7 +63,7 @@ webSvr.session("login.do", function(req, res) { //res.writeFile("/web/setting.htm"); //TODO: Error handler of undefined methods console.log(session); - res.redirect("/web/setting.htm"); + res.redirect("setting.htm"); }); } else { res.writeHead(401); @@ -93,11 +93,29 @@ webSvr.url("redirect", function(req, res) { Template: render template with params */ webSvr.url("template.node", function(req, res) { + webSvr.engine(require("dot").compile); + res.writeHead(200, {"Content-Type": "text/html"}); //render template with session: { "username" : "admin" } req.session.get(function(session) { - //TODO: Change to req.render(session); - res.render(req, session); + res.render(session); + }); +}); + +/* +Template: render template with jade +*/ +webSvr.url("template.jade", function(req, res) { + webSvr.engine(require("jade").compile); + + res.writeHead(200, {"Content-Type": "text/html"}); + res.render({ + pageTitle: "Testing Jade", + maintainer: { + name: 'Forbes Lindesay', + twitter: '@ForbesLindesay', + blog: 'forbeslindesay.co.uk' + } }); }); diff --git a/website/web/index.htm b/website/web/login.htm similarity index 100% rename from website/web/index.htm rename to website/web/login.htm diff --git a/website/web/template.jade b/website/web/template.jade new file mode 100644 index 0000000..ce66dc0 --- /dev/null +++ b/website/web/template.jade @@ -0,0 +1,16 @@ +doctype 5 +html(lang="en") + head + title= pageTitle + script(type='text/javascript'). + if (foo) bar(1 + 5) + body + h1 Jade - node template engine + #container.col + if youAreUsingJade + p You are amazing + else + p Get on it! + p. + Jade is a terse and simple templating language with a + strong focus on performance and powerful features. \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index d911195..63fdbe1 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -832,7 +832,8 @@ var WebSvr = module.exports = function(options) { */ var Template = (function() { - var engine = require("dot"); + //default engine + var engineFunc = require("dot").compile; //get a file var getFile = function(filename, cb) { @@ -847,7 +848,7 @@ var WebSvr = module.exports = function(options) { //render a file var render = function(chrunk, params, outFn) { try { - tmplFn = engine.compile(chrunk, params); + tmplFn = engineFunc(chrunk, params); outFn(tmplFn(params)); } catch(err) { Logger.debug(err); @@ -872,8 +873,8 @@ var WebSvr = module.exports = function(options) { render(tmpl, model, end); }); } - , engine: function(engineLib) { - engine = engineLib; + , engine: function(_engineFunc) { + engineFunc = _engineFunc; } } }()); From 229977a8fc5e00be60b95a70db33c8e6ae68ffe1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 27 Oct 2013 12:00:47 +0800 Subject: [PATCH 098/195] Add default model support; --- README.md | 15 +++++++++++---- package.json | 2 +- website/svr/sitetest.js | 9 +++++++++ website/web/header.xml | 3 +++ website/web/template.node | 1 + websvr/websvr.js | 28 +++++++++++++++++++++------- 6 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 website/web/header.xml diff --git a/README.md b/README.md index 8bb5441..1ea416a 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Note: -------------- Filter and Handler doesn't have the same match rules when you sending a request -Filter : Match any section in the request url, etc: +Filter : Match any section in the request url, for example websvr.filter(".svr", cb); @@ -106,7 +106,7 @@ The result is request: "domain.com/admin/root/login.svr" match: true -Handler : Match from the begining but it can bypass '/', etc: +Handler : Match from the begining but it can bypass '/', for example: websvr.handle("root/login", cb) //or websvr.handle("/root/login", cb) @@ -136,11 +136,18 @@ You can change template engine, webSvr.engine(engineFunc); -etc: +for example: webSvr.engine(require("doT").compile); webSvr.engine(require("jade").compile); +You can define some default properties in model, for example header/footer, this parameters will be overridden if they have the same name in your custom model. + + webSvr.model({ + title : "New Page" + , username: "kris" + , header : require("fs").readFileSync("web/header.xml") + }); Settings @@ -170,7 +177,7 @@ Settings API: , cache: true //enable debug information output , debug: true - //receive buffer, default size 32k, etc: receive post data from ajax request + //receive buffer, default size 32k, example: receive post data from ajax request , bufferSize: 32768 //default pages, only one is supported diff --git a/package.json b/package.json index 1ede88b..271b28f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.8", + "version": "0.1.9", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 742a65f..9e54e5b 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -89,6 +89,15 @@ webSvr.url("redirect", function(req, res) { res.redirect("/svr/websvr.all.js"); }); +/* +Template: define default template params +*/ +webSvr.model({ + title : "New Page" + , username: "kris" + , header : require("fs").readFileSync("web/header.xml") +}); + /* Template: render template with params */ diff --git a/website/web/header.xml b/website/web/header.xml new file mode 100644 index 0000000..9c5b982 --- /dev/null +++ b/website/web/header.xml @@ -0,0 +1,3 @@ +
    +

    This is global header, you can override it in your custom model.

    +
    \ No newline at end of file diff --git a/website/web/template.node b/website/web/template.node index 61f1cd3..00b2aba 100644 --- a/website/web/template.node +++ b/website/web/template.node @@ -6,6 +6,7 @@ Login + {{=it.header}} Hello: {{=it.username}}
    diff --git a/websvr/websvr.js b/websvr/websvr.js index 63fdbe1..b2d840b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -68,7 +68,7 @@ var WebSvr = module.exports = function(options) { , cache: true //enable debug information output , debug: true - //receive buffer, default size 255Kb, etc: receive post data from ajax request + //receive buffer, default size 255Kb, e.g., receive post data from ajax request , bufferSize: 261120 //default pages, only one is supported @@ -536,8 +536,8 @@ var WebSvr = module.exports = function(options) { /* Does this mapper matched this request? Filter and Handler doesn't have the same matched rules when you passing a string - Filter : Match any section of the request url, etc: websvr.filter(".svr", cb); - Handler : Match from the begining but it can bypass '/', etc: websvr.handle("root/login", cb) or websvr.handle("/root/login") + Filter : Match any section of the request url, e.g., websvr.filter(".svr", cb); + Handler : Match from the begining but it can bypass '/', e.g., websvr.handle("root/login", cb) or websvr.handle("/root/login") */ match: function(req, isHandler) { var self = this, @@ -832,8 +832,10 @@ var WebSvr = module.exports = function(options) { */ var Template = (function() { - //default engine - var engineFunc = require("dot").compile; + //default engine and defaultModel (e.g., define global footer/header in model) + var engineFunc = require("dot").compile + , defaultModel = null + ; //get a file var getFile = function(filename, cb) { @@ -846,9 +848,17 @@ var WebSvr = module.exports = function(options) { }; //render a file - var render = function(chrunk, params, outFn) { + var render = function(chrunk, model, outFn) { + var params; + if (defaultModel) { + params = Object.create(defaultModel); + _.extend(params, model); + } else { + params = model; + } + try { - tmplFn = engineFunc(chrunk, params); + tmplFn = engineFunc(chrunk); outFn(tmplFn(params)); } catch(err) { Logger.debug(err); @@ -876,6 +886,9 @@ var WebSvr = module.exports = function(options) { , engine: function(_engineFunc) { engineFunc = _engineFunc; } + , model: function(_model) { + defaultModel = _model; + } } }()); @@ -1048,6 +1061,7 @@ var WebSvr = module.exports = function(options) { //Template self.engine = Template.engine; + self.model = Template.model; //Get a full path of a request self.getFullPath = function(filePath) { From a8d45074fc66e323f3f99c325f9b5013346ed18e Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 27 Oct 2013 13:01:58 +0800 Subject: [PATCH 099/195] Make webSvr.render as public; --- README.md | 12 ++++++++++++ websvr/websvr.js | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ea416a..c13ec8d 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,18 @@ You can define some default properties in model, for example header/footer, this , header : require("fs").readFileSync("web/header.xml") }); +And more, you can use template and redner your it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.root; + + //define default model + var model = {}; + webSvr.model(model); + + //define header/footer + webSvr.render("header.tmpl", {categoryList: category.categoryList}, function(html) { + model.header = html; + console.log(model); + }); + Settings -------------- diff --git a/websvr/websvr.js b/websvr/websvr.js index b2d840b..b404d51 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -868,9 +868,9 @@ var WebSvr = module.exports = function(options) { return { //render templates - render: function(tmplUrl, model) { + render: function(tmplUrl, model, outFn) { var res = this, - end = res.end; + end = outFn || res.end; if (arguments.length == 1) { model = tmplUrl; @@ -1060,6 +1060,7 @@ var WebSvr = module.exports = function(options) { self.settings = Settings; //Template + self.render = Template.render; self.engine = Template.engine; self.model = Template.model; From 2783c8891cd1234099dac674e8963b51820e41a2 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 15 Nov 2013 22:11:36 +0800 Subject: [PATCH 100/195] Add custom 404 page. --- README.md | 2 ++ package.json | 2 +- websvr/websvr.js | 10 ++++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c13ec8d..82d9e86 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ Settings API: //default pages, only one is supported , defaultPage: "index.html" + //404 template/static file + , 404: "404.tmpl" //logger file path , logger: os.tmpDir() + "/log.txt" diff --git a/package.json b/package.json index 271b28f..f7469c5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.9", + "version": "0.1.10", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index b404d51..c9a8387 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -24,7 +24,7 @@ var formidable = require("formidable"); var _ = { //extend object to target extend: function(tar, obj) { - if (!obj) return; + if (!obj) return tar; for (var key in obj) { tar[key] = obj[key]; @@ -73,6 +73,7 @@ var WebSvr = module.exports = function(options) { //default pages, only one is supported , defaultPage: "index.html" + , 404: "" //logger file path , logger: os.tmpDir() + "/log.txt" @@ -1093,8 +1094,13 @@ var WebSvr = module.exports = function(options) { }; self.write404 = function(res) { + var tmpl404 = Settings["404"]; + res.writeHead(404, {"Content-Type": "text/html"}); - res.end("File not found!"); + + tmpl404 + ? res.render(tmpl404, null) + : res.end("File not found!"); return self; }; From 8d6099232d0969e6ed560a086d22d4520ffd0564 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 15 Dec 2013 21:41:45 +0800 Subject: [PATCH 101/195] Remove bufferSize, stored receives from Buffer to String when on("data") event triggered; Add default Content-Type: text/html if there is undefined before response.end; --- README.md | 2 -- websvr/websvr.js | 16 +++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 82d9e86..36f966c 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,6 @@ Settings API: , cache: true //enable debug information output , debug: true - //receive buffer, default size 32k, example: receive post data from ajax request - , bufferSize: 32768 //default pages, only one is supported , defaultPage: "index.html" diff --git a/websvr/websvr.js b/websvr/websvr.js index c9a8387..8e640c7 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -68,8 +68,6 @@ var WebSvr = module.exports = function(options) { , cache: true //enable debug information output , debug: true - //receive buffer, default size 255Kb, e.g., receive post data from ajax request - , bufferSize: 261120 //default pages, only one is supported , defaultPage: "index.html" @@ -163,18 +161,14 @@ var WebSvr = module.exports = function(options) { */ var BodyParser = function(req, res, callback) { - var buffer = new Buffer(Settings.bufferSize); - - var length = 0, data = ""; + var receives = ''; req.on('data', function(chunk) { - chunk.copy(buffer, length, 0, chunk.length); - length += chunk.length; + receives += chunk.toString('utf8'); }); req.on('end', function() { - data = length > 0 ? buffer.toString('utf8', 0, length) : ""; - callback(data); + callback(receives); }); }; @@ -989,6 +983,10 @@ var WebSvr = module.exports = function(options) { //Rewrite the write/writeHead functionalities of current response object var endFn = res.end; res.end = function() { + + //If Content-Type is undefined, using text/html as default + !res.headersSent && !res.getHeader('Content-Type') && res.setHeader("Content-Type", "text/html"); + //Execute old end endFn.apply(res, arguments); //Rewirte write/writeHead on response object From 479705e1823c2a483133ff1b64a662123ee276fc Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 15 Dec 2013 21:42:19 +0800 Subject: [PATCH 102/195] Update npm package; --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f7469c5..8de1314 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.10", + "version": "0.1.11", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 1cc6a4d615ae2be3a494d06561f981b6fa898ca1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 1 Jan 2014 22:31:11 +0800 Subject: [PATCH 103/195] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 36f966c..3683455 100644 --- a/README.md +++ b/README.md @@ -149,14 +149,15 @@ You can define some default properties in model, for example header/footer, this , header : require("fs").readFileSync("web/header.xml") }); -And more, you can use template and redner your it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.root; +And more, you can use template and render it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.root; - //define default model + //pre-defined model var model = {}; webSvr.model(model); - //define header/footer + //render a template using model, callbak argument is result html webSvr.render("header.tmpl", {categoryList: category.categoryList}, function(html) { + //store rendered html to header model.header = html; console.log(model); }); From 2e1f538e71ddd628af8741a290ee92f26c95307a Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 18 Jan 2014 18:11:23 +0800 Subject: [PATCH 104/195] Update: Using Buffer.concat --- package.json | 2 +- websvr/websvr.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8de1314..3369ba0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.11", + "version": "0.1.12", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 8e640c7..6154a74 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -161,14 +161,14 @@ var WebSvr = module.exports = function(options) { */ var BodyParser = function(req, res, callback) { - var receives = ''; + var receives = []; req.on('data', function(chunk) { - receives += chunk.toString('utf8'); + receives.push(chunk); }); req.on('end', function() { - callback(receives); + callback(Buffer.concat(receives).toString()); }); }; From b1f0e3a0aa0588a02613548be4f7ae4c6ddbb103 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Tue, 4 Mar 2014 19:13:53 +0800 Subject: [PATCH 105/195] Update SessionManager; --- websvr/websvr.js | 200 +++++++++++++++++++++++------------------------ 1 file changed, 99 insertions(+), 101 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 6154a74..c900677 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -5,6 +5,8 @@ * Project url: https://github.com/newghost/node-websvr */ +"use strict"; + //Node libraries var fs = require("fs"); var path = require("path"); @@ -180,11 +182,6 @@ var WebSvr = module.exports = function(options) { //session id self.sid = null; - //session stored object - self.obj = null; - //is this new session? - self.new = false; - //init session object self.init(req, res); }; @@ -204,102 +201,31 @@ var WebSvr = module.exports = function(options) { if (idx < 0 || sidVal.length != 25) { sidVal = SessionManager.create(); res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); - self.new = true; }; self.sid = sidVal; SessionManager.refresh(self.sid); } - //Create new session object - , newObj: function(key, cb) { - //Key is offered, return null of this key, else return empty session object - var self = this, - val = key ? null : {}; - - self.obj = {}; - cb && cb(val); - return val; - } - - //Get value from session object - , getVal: function(key, cb) { - var self = this; - - //key is null, return all the session object - var val = key ? self.obj[key] : self.obj; - cb && cb(val); - - return val; - } - //Set an key/value pair in session object , set: function(key, val, cb) { var self = this; - - //Get session object first - self.get(function() { - - //Add or update key/value in session object - self.obj[key] = val; - - //Write or modify json file - fs.writeFile(SessionManager.getPath(self.sid), JSON.stringify(self.obj), function(err) { - if (err) { - Logger.debug(err); - return; - } - - cb && cb(self.obj); - - //force update - SessionManager.update(self.sid); - }); - }); + SessionManager.set(self.sid, key, val, cb); } //Get value from session file , get: function(key, cb) { - var self = this; + var self = this + , val; //The first parameter is callback function if (key.constructor == Function) { cb = key; key = null; } + val = SessionManager.get(self.sid, key); - //The session object is already loaded - if (self.obj) return self.getVal(key, cb); - - //It's a new session file, need not to load it from file - if (self.new) return self.newObj(key, cb); - - var sessionPath = SessionManager.getPath(self.sid); - - //File operates, will cause delay - fs.exists(sessionPath, function(exists) { - //err: file doesn't exist - if (!exists) { - return self.newObj(key, cb); - - //session not expired - } else if (SessionManager.isValid(self.sid)) { - fs.readFile(sessionPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - }; - data = data || "{}"; - self.obj = JSON.parse(data); - - return self.getVal(key, cb); - }); - - //session expired, treat it as new session - } else { - return self.newObj(key, cb); - } - }); + return cb ? cb(val) : val; } }; @@ -394,9 +320,57 @@ var WebSvr = module.exports = function(options) { //timer var timer; - //session array object, stored with {sid: [update time]}; + /* + * session array object, alls: stored sessions and updated time + * etc: + { + sid: { + .... + __lastAccessTime: dateObject + } + } + */ var list = {}; + /* + Init the sessions, load into session pool + */ + var init = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return Logger.debug(err); + + //converted to minutes + var expire = (+new Date() - gcTime) / 60000 | 0; + + files.forEach(function(file) { + if (file.length == 25) { + var stamp = parseInt(file.substr(0, file.indexOf('-'))) + , sessionPath = SessionManager.getPath(file); + + if (stamp) { + //remove the expired session + if (stamp < expire) { + remove(file); + } else { + fs.readFile(sessionPath, function(err, data) { + if (err) { + Logger.debug(err); + return; + } + + try { + list[file] = JSON.parse(data); + } catch (e) { + Logger.debug(e); + } + }); + } + } + } + }); + }); + }; + var getPath = function(sid) { return path.join(Settings.sessionDir, sid); }; @@ -413,12 +387,14 @@ var WebSvr = module.exports = function(options) { //fix the length to 25 uuid += '00000000000000000000'.substr(0, 25 - uuid.length); + list[uuid] = {}; + return uuid; }; //force update session in list var update = function(sid, datetime) { - list[sid] = datetime || new Date(); + list[sid].__lastAccessTime = datetime || new Date(); }; //remove a sesson from list @@ -437,11 +413,7 @@ var WebSvr = module.exports = function(options) { i.e. When WebSvr restarted, session will not expired. */ var isValid = function(sid) { - var now = new Date(); - - !list[sid] && (list[sid] = now); - - return now - list[sid] <= Settings.sessionTimeout + return list[sid] && ((new Date() - list[sid].__lastAccessTime) || 0 <= Settings.sessionTimeout); }; /* @@ -472,7 +444,7 @@ var WebSvr = module.exports = function(options) { stamp < expire ? remove(file) : Logger.debug("session skipped", file); - } + } } }); }); @@ -497,17 +469,41 @@ var WebSvr = module.exports = function(options) { timer = setInterval(cleanHandler, gcTime); }; + var get = function(sid, key) { + var session = list[sid] || {}; + return key ? session : session[key]; + }; + + var set = function(sid, key, val, cb) { + var session = list[sid] || {}; + session[key] = val; + //force update + SessionManager.update(sid); + fs.writeFile(SessionManager.getPath(sid), JSON.stringify(session), function(err) { + if (err) { + Logger.debug(err); + } + + cb && cb(session); + }); + }; + + init(); + return { - list: list, - create: create, - update: update, - remove: remove, - refresh: refresh, - isValid: isValid, - getPath: getPath, - clean: clean, - start: start, - stop: stop + init: init + , list: list + , create: create + , update: update + , remove: remove + , refresh: refresh + , isValid: isValid + , getPath: getPath + , clean: clean + , start: start + , stop: stop + , get: get + , set: set } })(); @@ -568,7 +564,7 @@ var WebSvr = module.exports = function(options) { parse: boolean */ extend: function(options) { - for(key in options) { + for(var key in options) { this[key] = options[key] } } @@ -844,7 +840,9 @@ var WebSvr = module.exports = function(options) { //render a file var render = function(chrunk, model, outFn) { - var params; + var params + , tmplFn; + if (defaultModel) { params = Object.create(defaultModel); _.extend(params, model); From 4a8a3896c1b201b175c4eee3e60eefa534dcf1eb Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 4 Mar 2014 23:05:34 +0800 Subject: [PATCH 106/195] Optimize session manager; --- package.json | 2 +- websvr/websvr.js | 35 ++++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 3369ba0..dd95f5e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.12", + "version": "0.1.13", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index c900677..b01fb76 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -315,10 +315,8 @@ var WebSvr = module.exports = function(options) { var SessionManager = (function() { //duration time - var gcTime = Settings.sessionTimeout + Settings.sessionGarbage; - - //timer - var timer; + var gcTime + , timer; /* * session array object, alls: stored sessions and updated time @@ -336,6 +334,10 @@ var WebSvr = module.exports = function(options) { Init the sessions, load into session pool */ var init = function() { + gcTime = Settings.sessionTimeout + Settings.sessionGarbage; + + console.log('Session Dir (gc time):', Settings.sessionDir, gcTime); + fs.readdir(Settings.sessionDir, function(err, files) { if (err) return Logger.debug(err); @@ -388,13 +390,14 @@ var WebSvr = module.exports = function(options) { uuid += '00000000000000000000'.substr(0, 25 - uuid.length); list[uuid] = {}; + update(uuid); return uuid; }; - //force update session in list + //force update session in list, convert to big int var update = function(sid, datetime) { - list[sid].__lastAccessTime = datetime || new Date(); + list[sid].__lastAccessTime = +Date.parse(datetime) || +new Date(); }; //remove a sesson from list @@ -470,16 +473,26 @@ var WebSvr = module.exports = function(options) { }; var get = function(sid, key) { - var session = list[sid] || {}; - return key ? session : session[key]; + var session = list[sid]; + + if (!isValid(sid)) { + return ''; + } + update(sid); + return key ? session[key] : session; }; var set = function(sid, key, val, cb) { - var session = list[sid] || {}; + var session = list[sid]; session[key] = val; + + if (!isValid(sid)) { + return; + } + //force update - SessionManager.update(sid); - fs.writeFile(SessionManager.getPath(sid), JSON.stringify(session), function(err) { + update(sid); + fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { if (err) { Logger.debug(err); } From e0a86700ced3651cdc8a128010450016df3625ee Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 4 Mar 2014 23:45:21 +0800 Subject: [PATCH 107/195] Fix bugs of Session Manager; --- package.json | 2 +- websvr/websvr.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index dd95f5e..eacb34b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.13", + "version": "0.1.14", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index b01fb76..1aaea3a 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -453,9 +453,14 @@ var WebSvr = module.exports = function(options) { }); }; - //refresh session in list, valid first, if not expired, update the time + //refresh session in list, valid first, if not expired, update the time, if not exist create new one var refresh = function(sid, datetime) { - isValid(sid) && update(sid, datetime); + if (!list[sid]) { + list[sid] = {}; + update(sid, datetime); + } else { + isValid(sid) && update(sid, datetime); + } }; var stop = function() { @@ -473,7 +478,7 @@ var WebSvr = module.exports = function(options) { }; var get = function(sid, key) { - var session = list[sid]; + var session = list[sid] || {}; if (!isValid(sid)) { return ''; @@ -483,7 +488,7 @@ var WebSvr = module.exports = function(options) { }; var set = function(sid, key, val, cb) { - var session = list[sid]; + var session = list[sid] || {}; session[key] = val; if (!isValid(sid)) { From a1f9b3ef5a3f34deac556ff5878b791dbfc9f933 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 12 Mar 2014 22:23:30 +0800 Subject: [PATCH 108/195] Add include file and template cache; --- README.md | 13 +++++++++++++ package.json | 2 +- websvr/websvr.js | 46 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3683455..603397b 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,17 @@ And more, you can use template and render it by using websvr.render(tmplPath, mo console.log(model); }); +Include file, you can using "#include" to include a file during rendering a template, in order to make the process easier, the file will fetched from the cache pool so the first refresh will not work after restart the server; + + + +
    + +Cache templates, by default, server will cache the templates(include the "include file" in the templates), turn it off via: + + var webSvr = new WebSvr({ + templateCache: false + }).start(); Settings -------------- @@ -190,6 +201,8 @@ Settings API: , cache: true //enable debug information output , debug: true + //enable cache of template/include file (when enabled templates will not be refreshed before restart) + , templateCache: true //default pages, only one is supported , defaultPage: "index.html" diff --git a/package.json b/package.json index eacb34b..0e730b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.14", + "version": "0.1.15", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 1aaea3a..a3499a0 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -70,6 +70,8 @@ var WebSvr = module.exports = function(options) { , cache: true //enable debug information output , debug: true + //enable cache of template/include file (when enabled templates will not be refreshed before restart) + , templateCache: true //default pages, only one is supported , defaultPage: "index.html" @@ -841,6 +843,13 @@ var WebSvr = module.exports = function(options) { */ var Template = (function() { + //Caching of template files. + var templatePool = {} + , includeRegExp = //g + , includeBeginLen = 14 + , includeAfterLen = 4 + ; + //default engine and defaultModel (e.g., define global footer/header in model) var engineFunc = require("dot").compile , defaultModel = null @@ -850,9 +859,38 @@ var WebSvr = module.exports = function(options) { var getFile = function(filename, cb) { var fullpath = path.join(Settings.root, filename); - fs.readFile(fullpath, function(err, html) { - err && Logger.debug(err); - err ? cb("") : cb(html); + //if template cache enabled, get from cache pool directly + if (Settings.templateCache && templatePool[filename]) { + cb(templatePool[filename]); + } else { + fs.readFile(fullpath, function(err, tmpl) { + if (err) { + Logger.debug(err); + cb && cb(""); + } else { + tmpl = tmpl.toString(); + templatePool[filename] = tmpl; + Logger.debug('update template cache', filename); + cb && cb(tmpl); + } + }); + } + }; + + var getTemplate = function(filename, cb) { + getFile(filename, function(tmpl) { + /* + find and update all the include files, + will get templates from cache for making the process easier, + the first refresh will not work, need some time to update the cache pool + */ + tmpl = tmpl.replace(includeRegExp, function(fileStr) { + var includeFile = fileStr.substring(includeBeginLen, fileStr.length - includeAfterLen); + getFile(includeFile); + return templatePool[includeFile] || ''; + }); + + cb(tmpl); }); }; @@ -890,7 +928,7 @@ var WebSvr = module.exports = function(options) { tmplUrl.indexOf('?') > -1 && (tmplUrl = tmplUrl.substr(0, tmplUrl.indexOf('?'))); } - getFile(tmplUrl, function(tmpl) { + getTemplate(tmplUrl, function(tmpl) { render(tmpl, model, end); }); } From ce940982484e59ace93c80a2ecd0eb94d46ba6db Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 12 Mar 2014 22:29:35 +0800 Subject: [PATCH 109/195] Update READEM for include file; --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 603397b..a54115e 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,8 @@ And more, you can use template and render it by using websvr.render(tmplPath, mo Include file, you can using "#include" to include a file during rendering a template, in order to make the process easier, the file will fetched from the cache pool so the first refresh will not work after restart the server; +###Be ware: include file path relative to web root, not the template file itself.### +
    From 98bcebf8b3d793c4e0102d068e4798555de0aeca Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 13 Mar 2014 10:23:09 +0800 Subject: [PATCH 110/195] change undefined session value to ''(empty string); --- README.md | 7 ++++++- websvr/websvr.js | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a54115e..6d2f082 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ It's simple to start the websvr. Filter (HttpModule) -------------- -Session based authentication, basically useage: +Session based authentication, will return '' if session value is undefined, basically useage: /* General filter: parse the post data / session before all request @@ -50,6 +50,11 @@ Session based authentication, basically useage: Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" + /* + API, pass value of key to callback [optional] + */ + var value = req.session.get(key [, callback]); + /* Session Filter: protect web/* folder => (validation by session); */ diff --git a/websvr/websvr.js b/websvr/websvr.js index a3499a0..65902e2 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -226,8 +226,9 @@ var WebSvr = module.exports = function(options) { key = null; } val = SessionManager.get(self.sid, key); + cb && cb(val); - return cb ? cb(val) : val; + return val; } }; @@ -338,7 +339,7 @@ var WebSvr = module.exports = function(options) { var init = function() { gcTime = Settings.sessionTimeout + Settings.sessionGarbage; - console.log('Session Dir (gc time):', Settings.sessionDir, gcTime); + Logger.debug('Session Dir (gc time):', Settings.sessionDir, gcTime); fs.readdir(Settings.sessionDir, function(err, files) { if (err) return Logger.debug(err); @@ -486,7 +487,9 @@ var WebSvr = module.exports = function(options) { return ''; } update(sid); - return key ? session[key] : session; + var val = session[key]; + typeof val == 'undefined' && (val = ''); + return key ? val : session; }; var set = function(sid, key, val, cb) { From 3a0e0ad18ce24f8185ed6c73ac0464a31113db57 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 13 Mar 2014 10:49:12 +0800 Subject: [PATCH 111/195] Update sessions; --- README.md | 2 +- websvr/websvr.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6d2f082..2770b02 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ It's simple to start the websvr. Filter (HttpModule) -------------- -Session based authentication, will return '' if session value is undefined, basically useage: +Session based authentication, basically useage: /* General filter: parse the post data / session before all request diff --git a/websvr/websvr.js b/websvr/websvr.js index 65902e2..86763c8 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -487,9 +487,7 @@ var WebSvr = module.exports = function(options) { return ''; } update(sid); - var val = session[key]; - typeof val == 'undefined' && (val = ''); - return key ? val : session; + return key ? session[key] : session; }; var set = function(sid, key, val, cb) { From db8a83994a5db9c9972302576397b061f0aa8fa5 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 15 Mar 2014 20:59:13 +0800 Subject: [PATCH 112/195] Fix bugs: callback is undefined; --- package.json | 2 +- websvr/websvr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0e730b0..889a474 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.15", + "version": "0.1.16", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 86763c8..f40012b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -862,7 +862,7 @@ var WebSvr = module.exports = function(options) { //if template cache enabled, get from cache pool directly if (Settings.templateCache && templatePool[filename]) { - cb(templatePool[filename]); + cb && cb(templatePool[filename]); } else { fs.readFile(fullpath, function(err, tmpl) { if (err) { From f10bf422e883841fbfaaec2e5208c607c61ff422 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 12 Apr 2014 22:13:13 +0800 Subject: [PATCH 113/195] Add session domain support; --- README.md | 3 +++ websvr/websvr.js | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2770b02..840c187 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,9 @@ Settings API: //session file stored here , sessionDir: os.tmpDir() + //session domain, e.g. ".google.com" + , sessionDomain: "" + //tempary upload file stored here , uploadDir: os.tmpDir() }; diff --git a/websvr/websvr.js b/websvr/websvr.js index f40012b..344c7f7 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -95,6 +95,9 @@ var WebSvr = module.exports = function(options) { //session file stored here , sessionDir: os.tmpDir() + //session domain + , sessionDomain: '' + //tempary upload file stored here , uploadDir: os.tmpDir() }; @@ -190,9 +193,11 @@ var WebSvr = module.exports = function(options) { SessionParser.prototype = { init: function(req, res) { - var self = this, - sidKey = "_wsid", - sidVal; + var self = this + , sidKey = "_wsid" + , sidVal + , sidStr + ; //Get or Create sid, sid exist in the cookie, read it var cookie = req.headers.cookie || ""; @@ -202,7 +207,10 @@ var WebSvr = module.exports = function(options) { //Sid doesn't exist, create it if (idx < 0 || sidVal.length != 25) { sidVal = SessionManager.create(); - res.setHeader("Set-Cookie", " _wsid=" + sidVal + "; path=/"); + sidStr = " _wsid=" + sidVal + "; path=/"; + Settings.sessionDomain && (sidStr += "; domain=" + Settings.sessionDomain); + + res.setHeader("Set-Cookie", sidStr); }; self.sid = sidVal; From 09c0e22693032268485b7af239bd30c631dc6519 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 12 Apr 2014 22:13:39 +0800 Subject: [PATCH 114/195] Update version; --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 889a474..23a0459 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.16", + "version": "0.1.17", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From f625b653f5562ec4e6d1a0aa290c4eecf4b07f37 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 20 Apr 2014 21:34:30 +0800 Subject: [PATCH 115/195] Add parseCookie support; --- README.md | 7 +++++++ websvr/websvr.js | 29 ++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 840c187..a7a5049 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,13 @@ The result is: You can use regular expression to match part of url in Handler. +Cookies +-------------- + + //get cookie value by key + req.cookies[key]; + + Template -------------- Render template with params, using doT template engine diff --git a/websvr/websvr.js b/websvr/websvr.js index 344c7f7..6630533 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -200,12 +200,10 @@ var WebSvr = module.exports = function(options) { ; //Get or Create sid, sid exist in the cookie, read it - var cookie = req.headers.cookie || ""; - var idx = cookie.indexOf(sidKey + "="); - (idx >= 0) && (sidVal = cookie.substring(idx + 6, idx + 31)); + var sidVal = req.cookies[sidKey]; //Sid doesn't exist, create it - if (idx < 0 || sidVal.length != 25) { + if (sidVal.length != 25 || !SessionManager.isValid(sidVal)) { sidVal = SessionManager.create(); sidStr = " _wsid=" + sidVal + "; path=/"; Settings.sessionDomain && (sidStr += "; domain=" + Settings.sessionDomain); @@ -315,7 +313,28 @@ var WebSvr = module.exports = function(options) { }; }; - parseFile(); + /* + parse cookie in request + */ + var parseCookies = function() { + var cookie = req.headers.cookie; + if (cookie) { + var cookieArr = cookie.split(';') + , cookies = {} + ; + for (var i = 0; i < cookieArr.length; i++) { + var strCookie = cookieArr[i] + , idx = strCookie.indexOf('=') + ; + idx > 0 && (cookies[strCookie.substr(0, idx).trim()] = strCookie.substr(idx + 1).trim()); + } + } + req.cookies = cookies; + + parseFile(); + }; + + parseCookies(); }; /* From 38d2756ca5723b3923ae0507458998b54f2f3bc9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 20 Apr 2014 21:53:31 +0800 Subject: [PATCH 116/195] update cookie; --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 6630533..4c8ccd8 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -203,7 +203,7 @@ var WebSvr = module.exports = function(options) { var sidVal = req.cookies[sidKey]; //Sid doesn't exist, create it - if (sidVal.length != 25 || !SessionManager.isValid(sidVal)) { + if (!sidVal || sidVal.length != 25 || !SessionManager.isValid(sidVal)) { sidVal = SessionManager.create(); sidStr = " _wsid=" + sidVal + "; path=/"; Settings.sessionDomain && (sidStr += "; domain=" + Settings.sessionDomain); From c5ef75b5f56ae0e2d3c291d5437619e7e2f602c9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Mon, 21 Apr 2014 21:00:13 +0800 Subject: [PATCH 117/195] Fix bug: req.cookies may undefined; --- websvr/websvr.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 4c8ccd8..2ed7e0b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -317,11 +317,13 @@ var WebSvr = module.exports = function(options) { parse cookie in request */ var parseCookies = function() { - var cookie = req.headers.cookie; + var cookie = req.headers.cookie + , cookies = {} + ; + if (cookie) { - var cookieArr = cookie.split(';') - , cookies = {} - ; + var cookieArr = cookie.split(';'); + for (var i = 0; i < cookieArr.length; i++) { var strCookie = cookieArr[i] , idx = strCookie.indexOf('=') @@ -329,8 +331,8 @@ var WebSvr = module.exports = function(options) { idx > 0 && (cookies[strCookie.substr(0, idx).trim()] = strCookie.substr(idx + 1).trim()); } } - req.cookies = cookies; + req.cookies = cookies; parseFile(); }; From 880b6f5e649039e94f1d69f69fdf88f34890fd26 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Tue, 10 Jun 2014 16:55:40 +0800 Subject: [PATCH 118/195] Update create method of Session ID; --- websvr/websvr.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 2ed7e0b..a138dfc 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -411,20 +411,18 @@ var WebSvr = module.exports = function(options) { //create a new session id var create = function() { - //Time stamp, change interval is 18.641 hours, higher 6 bits will be kept, this is used for delete the old sessions - var uuid - = ((+new Date()) / 60000 | 0) //Time stamp, change interval is 1 min, 8 chars - + '-' - + ((Math.random() * 0x4000000 | 0)) //Random 1: Used for distinguish the session, max 8 chars - + ((Math.random() * 0x4000000 | 0)); //Random 2: Used for distinguish the session, max 8 chars - - //fix the length to 25 - uuid += '00000000000000000000'.substr(0, 25 - uuid.length); + /* + * (Time stamp - [random character ...]).length = 25 + */ + var id = (+new Date()).toString('32') + '-'; + for (var i = id.length; i < 25; i++ ) { + id += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ + } - list[uuid] = {}; - update(uuid); + list[id] = {}; + update(id); - return uuid; + return id; }; //force update session in list, convert to big int @@ -435,7 +433,9 @@ var WebSvr = module.exports = function(options) { //remove a sesson from list var remove = function(sid) { //delete the file - fs.unlink(getPath(sid)); + fs.unlink(getPath(sid), function(err) { + Logger.debug("unlink session file err", err); + }); //remove from list delete list[sid]; @@ -485,7 +485,11 @@ var WebSvr = module.exports = function(options) { }); }; - //refresh session in list, valid first, if not expired, update the time, if not exist create new one + /* + * refresh session in list valid first, + * if not expired update the time + * if not exist create new one + */ var refresh = function(sid, datetime) { if (!list[sid]) { list[sid] = {}; @@ -645,7 +649,7 @@ var WebSvr = module.exports = function(options) { var mapper = new Mapper(expression, handler, options); Filter.filters.push(mapper); - return self; + return this; } /* @@ -657,7 +661,7 @@ var WebSvr = module.exports = function(options) { //insert at the top of the filter array Filter.filters.splice(0, 0, mapper); - return self; + return this; } }; From 67aa5ade56f8a6435f9360fa2589362f454c4b5a Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 11 Jun 2014 18:17:54 +0800 Subject: [PATCH 119/195] Pickup parameters in url expression; --- README.md | 8 +++++- websvr/websvr.js | 73 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a7a5049..329e325 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ Return request object WebSvr APIs -------------- -Mapping url to file +Mapping url to file, webSvr.url equal to webSvr.handle webSvr.url("sitetest", ["svr/sitetest.js"]); @@ -319,6 +319,12 @@ Multi-Mapping in Handler or Filter res.writeFile(req.url + ".shtml"); }, {post: true}); +Pickup parameters from url expression + + webSvr.handle("/verify/:id", function(req, res) { + var id = req.params.id; + }); + Multi-instance support -------------- diff --git a/websvr/websvr.js b/websvr/websvr.js index a138dfc..8e0cf00 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -585,8 +585,10 @@ var WebSvr = module.exports = function(options) { Handler : Match from the begining but it can bypass '/', e.g., websvr.handle("root/login", cb) or websvr.handle("/root/login") */ match: function(req, isHandler) { - var self = this, - expression = self.expression; + var self = this + , reqUrl = req.url + , expression = self.expression + ; //No expression? It's a general filter mapper if (!expression) return true; @@ -594,22 +596,58 @@ var WebSvr = module.exports = function(options) { switch (expression.constructor) { //String handler must start with root path, but it can bypass '/' case String: - var idx = req.url.indexOf(expression); - return isHandler ? (idx == 0 || idx == 1) : (idx > -1); - case RegExp: return expression.test(req.url); + return self.matchString(req, isHandler, expression); + case RegExp: return expression.test(reqUrl); case Array: for (var i = 0, l = expression.length; i < l; i++) { - var idx = req.url.indexOf(expression[i]) - , tag = isHandler ? (idx == 0 || idx == 1) : (idx > -1); - if (tag) return true; + if (self.matchString(req, isHandler, expression[i])) { + return true; + } } return false; - } return false; }, + /* + Handle string expression like: /login/:username or /userinfo/ + */ + matchString: function(req, isHandler, expression) { + var reqUrl = req.url; + + //Pure string without params + if (expression.indexOf('/:') < 0) { + var idx = reqUrl.indexOf(expression); + return isHandler ? (idx == 0 || idx == 1) : (idx > -1); + //Handle and pickup params + } else { + //Remove the params in querystring + var idx = reqUrl.indexOf('?'); + idx > 0 && (reqUrl = reqUrl.substr(0, idx)); + + var parts = expression.split('/') + , start = expression.charAt(0) === '/' ? 0 : 1 + , urls = reqUrl.split('/') + , params = {} + ; + + for (var i = 0, l = parts.length; i < l; i++) { + var part = parts[i] + , url = urls[i + start] + ; + + if (part.charAt(0) === ':') { + params[part.substr(1)] = url; + } else if (part != url) { + return false; + } + } + _.extend(req.params, params); + return true; + } + }, + /* Add optional parameters on current mapper i.e: @@ -680,9 +718,10 @@ var WebSvr = module.exports = function(options) { FilterChain.prototype = { next: function() { - var self = this, - req = self.req, - res = self.res; + var self = this + , req = self.req + , res = self.res + ; var mapper = Filter.filters[self.idx++]; @@ -703,7 +742,7 @@ var WebSvr = module.exports = function(options) { //filter matched, parse the request and then execute it Parser(req, res, mapper); - }else{ + } else { //filter not matched, validate next filter self.next(); } @@ -982,8 +1021,9 @@ var WebSvr = module.exports = function(options) { var fileHandler = function(req, res) { - var url = req.url, - hasQuery = url.indexOf("?"); + var url = req.url + , hasQuery = url.indexOf("?") + ; //fs.stat can't recognize the file name with querystring; url = hasQuery > 0 ? url.substring(0, hasQuery) : url; @@ -1102,6 +1142,9 @@ var WebSvr = module.exports = function(options) { //render template objects res.render = Template.render; + //params in the matched url + req.params = {}; + //initial httprequest var filterChain = new FilterChain(function() { From 934a51842c22f19faffa31f4af3ef8d12c992c07 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 12 Jun 2014 12:36:25 +0800 Subject: [PATCH 120/195] Add new API: web.parseUrl(expression, reqUrl) --- README.md | 8 +++++++ websvr/websvr.js | 55 ++++++++++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 329e325..2014b3f 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,14 @@ Pickup parameters from url expression var id = req.params.id; }); +Parse parameters in url + + * expression = /home/:key/:pager + * /home/JavaScript => { id: 'JavaScript', pager: '' } + * /key/JavaScript => false + + var params = webSvr.parseUrl(expression, reqUrl); + Multi-instance support -------------- diff --git a/websvr/websvr.js b/websvr/websvr.js index 8e0cf00..598353e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -622,30 +622,42 @@ var WebSvr = module.exports = function(options) { return isHandler ? (idx == 0 || idx == 1) : (idx > -1); //Handle and pickup params } else { - //Remove the params in querystring - var idx = reqUrl.indexOf('?'); - idx > 0 && (reqUrl = reqUrl.substr(0, idx)); - - var parts = expression.split('/') - , start = expression.charAt(0) === '/' ? 0 : 1 - , urls = reqUrl.split('/') - , params = {} - ; + var params = this.parseUrl(expression, reqUrl); + params && _.extend(req.params, params); + return params; + } + }, - for (var i = 0, l = parts.length; i < l; i++) { - var part = parts[i] - , url = urls[i + start] - ; + /* + * Pickup the params in the request url + * expression = /home/:key/:pager + * /home/JavaScript => { id: 'JavaScript', pager: '' } + * /key/JavaScript => false + */ + parseUrl: function(expression, reqUrl) { + //Remove the params in querystring + var idx = reqUrl.indexOf('?'); + idx > 0 && (reqUrl = reqUrl.substr(0, idx)); + + var parts = expression.split('/') + , start = expression.charAt(0) === '/' ? 0 : 1 + , urls = reqUrl.split('/') + , params = {} + ; - if (part.charAt(0) === ':') { - params[part.substr(1)] = url; - } else if (part != url) { - return false; - } + for (var i = 0, l = parts.length; i < l; i++) { + var part = parts[i] + , url = urls[i + start] + ; + + if (part.charAt(0) === ':') { + params[part.substr(1)] = decodeURIComponent(url) || ''; + } else if (part != url) { + return false; } - _.extend(req.params, params); - return true; } + + return params; }, /* @@ -1176,6 +1188,9 @@ var WebSvr = module.exports = function(options) { }; //API have function chain + //Mapper + self.parseUrl = Mapper.prototype.parseUrl; + //Filter self.filter = Filter.filter; self.file = Filter.file; From fc513efa132298bc5604cd9614260aead8488390 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 15 Jun 2014 19:07:16 +0800 Subject: [PATCH 121/195] Fix bug: URIError: URI malformed --- websvr/websvr.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 598353e..d251de3 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -651,7 +651,12 @@ var WebSvr = module.exports = function(options) { ; if (part.charAt(0) === ':') { - params[part.substr(1)] = decodeURIComponent(url) || ''; + var paramName = part.substr(1); + try { + params[paramName] = decodeURIComponent(url) || ''; + } catch { + params[paramName] = url; + } } else if (part != url) { return false; } From d4e08a87f4254609f68338ce01d6f36aa56a8132 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 15 Jun 2014 19:10:13 +0800 Subject: [PATCH 122/195] Fix bug: URIError: URI malformed --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index d251de3..89fe59a 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -654,7 +654,7 @@ var WebSvr = module.exports = function(options) { var paramName = part.substr(1); try { params[paramName] = decodeURIComponent(url) || ''; - } catch { + } catch(err) { params[paramName] = url; } } else if (part != url) { From 72b0677fea26cd0bf737b53214a75ff8ac753683 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Tue, 24 Jun 2014 10:52:44 +0800 Subject: [PATCH 123/195] update version in NPM; --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 23a0459..2255cd3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.17", + "version": "0.1.18", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 83310a463cbca7c6ce4501b26fad0edeeb103a89 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 27 Jun 2014 14:54:38 +0800 Subject: [PATCH 124/195] Rename root to home: web home --- websvr/websvr.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 89fe59a..5c3e8ef 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -52,8 +52,8 @@ var WebSvr = module.exports = function(options) { Configurations */ var Settings = { - //root folder of web - root: "../" + //home folder of web + home: "../" //http start //default port of http @@ -582,7 +582,7 @@ var WebSvr = module.exports = function(options) { Does this mapper matched this request? Filter and Handler doesn't have the same matched rules when you passing a string Filter : Match any section of the request url, e.g., websvr.filter(".svr", cb); - Handler : Match from the begining but it can bypass '/', e.g., websvr.handle("root/login", cb) or websvr.handle("/root/login") + Handler : Match from the begining but it can bypass '/', e.g., websvr.handle("home/login", cb) or websvr.handle("/home/login") */ match: function(req, isHandler) { var self = this @@ -594,7 +594,7 @@ var WebSvr = module.exports = function(options) { if (!expression) return true; switch (expression.constructor) { - //String handler must start with root path, but it can bypass '/' + //String handler must start with home path, but it can bypass '/' case String: return self.matchString(req, isHandler, expression); case RegExp: return expression.test(reqUrl); @@ -947,7 +947,7 @@ var WebSvr = module.exports = function(options) { //get a file var getFile = function(filename, cb) { - var fullpath = path.join(Settings.root, filename); + var fullpath = path.join(Settings.home, filename); //if template cache enabled, get from cache pool directly if (Settings.templateCache && templatePool[filename]) { @@ -1034,7 +1034,7 @@ var WebSvr = module.exports = function(options) { /*****************Web initial codes*************/ //Parameters - var root; + var home; var fileHandler = function(req, res) { @@ -1045,7 +1045,7 @@ var WebSvr = module.exports = function(options) { //fs.stat can't recognize the file name with querystring; url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - var fullPath = path.join(root, url); + var fullPath = path.join(home, url); //Handle path var handlePath = function(phyPath) { @@ -1140,7 +1140,7 @@ var WebSvr = module.exports = function(options) { }; }; - //relative path, relative to the web root + //relative path, relative to the web home res.writeFile = function(filePath, cb) { self.writeFile(res, filePath, cb); }; @@ -1216,12 +1216,12 @@ var WebSvr = module.exports = function(options) { //Get a full path of a request self.getFullPath = function(filePath) { - return path.join(root, filePath); + return path.join(home, filePath); }; //Write file, filePath is relative path self.writeFile = function(res, filePath, cb, isSvrPath) { - !isSvrPath && (filePath = path.join(root, filePath)); + !isSvrPath && (filePath = path.join(home, filePath)); fs.exists(filePath, function(exist) { if (exist) { writeFile(res, filePath); @@ -1259,7 +1259,7 @@ var WebSvr = module.exports = function(options) { //Update the default value of Settings _.extend(Settings, options); - root = Settings.root; + home = Settings.home; //Create http server: Enable by default if (Settings.port) { @@ -1269,8 +1269,8 @@ var WebSvr = module.exports = function(options) { httpSvr.listen(port); Logger.log("Http server running at" - ,"Root:", root - ,"Port:", port + ,"home:", home + ,"port:", port ); self.httpSvr = httpSvr; @@ -1289,8 +1289,8 @@ var WebSvr = module.exports = function(options) { }, requestHandler).listen(httpsPort); Logger.log("Https server running at" - ,"Root:", root - ,"Port:", httpsPort + ,"home:", home + ,"port:", httpsPort ); self.httpsSvr = httpsSvr; From 77a6b646d2814207aa48307e71deaae0335f69cb Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 27 Jun 2014 14:57:13 +0800 Subject: [PATCH 125/195] Update README --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2014b3f..d2a9a6e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It's simple to start the websvr. //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 var webSvr = new WebSvr({ - root: "./" + home: "./" , listDir: true , debug: true }).start(); @@ -140,10 +140,14 @@ View is optional, in this case it will get the template path from req.url res.render({json: true}); -View is a relative path, relative to root web dir +View is a relative path, relative to web home res.render("list.tmpl", {json: true}); +View is a absolute path, relative to web root + + res.render("/list.tmpl", {json: true}); + You can change template engine, webSvr.engine(engineFunc); @@ -161,7 +165,7 @@ You can define some default properties in model, for example header/footer, this , header : require("fs").readFileSync("web/header.xml") }); -And more, you can use template and render it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.root; +And more, you can use template and render it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.home; //pre-defined model var model = {}; @@ -176,7 +180,7 @@ And more, you can use template and render it by using websvr.render(tmplPath, mo Include file, you can using "#include" to include a file during rendering a template, in order to make the process easier, the file will fetched from the cache pool so the first refresh will not work after restart the server; -###Be ware: include file path relative to web root, not the template file itself.### +###Be ware: include file path relative to web home, not the template file itself.### @@ -197,8 +201,8 @@ Return configuration of current WebSvr instance Settings API: var Settings = { - //root folder of web - root: "../" + //home folder of web + home: "../" //http start //default port of http @@ -252,7 +256,7 @@ Response -------------- Extension on reponse object -Ouput file, relative path, relative to the web root +Ouput file, relative path, relative to the web home res.writeFile(filePath, [callback]); @@ -339,7 +343,7 @@ Multi-instance support Start a https server, make sure that the port will no conflict with others. var httpsSvr = new WebSvr({ - root: "./" + home: "./" //disable http server , port: null From 2a2a1f3b2463e3bc74f5668631f39fab7af05dc9 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 27 Jun 2014 15:26:03 +0800 Subject: [PATCH 126/195] Add Settings.root --- README.md | 2 ++ websvr/test/testString.js | 11 +++++++++++ websvr/websvr.js | 20 ++++++++++++++++---- 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 websvr/test/testString.js diff --git a/README.md b/README.md index d2a9a6e..c900f3d 100644 --- a/README.md +++ b/README.md @@ -142,10 +142,12 @@ View is optional, in this case it will get the template path from req.url View is a relative path, relative to web home + //means related to Setting.home res.render("list.tmpl", {json: true}); View is a absolute path, relative to web root + //means related to Setting.root res.render("/list.tmpl", {json: true}); You can change template engine, diff --git a/websvr/test/testString.js b/websvr/test/testString.js new file mode 100644 index 0000000..05e2bf7 --- /dev/null +++ b/websvr/test/testString.js @@ -0,0 +1,11 @@ +var test = require("./lib/test"); + +var testString = "/a=link&b=1&c=abc&f=efg" + +test("Test string chartAt", function() { + testString.charAt(0) == '/' +}); + +test("Test string array", function() { + testString[0] == '/' +}); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 5c3e8ef..328e19b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -52,8 +52,11 @@ var WebSvr = module.exports = function(options) { Configurations */ var Settings = { + //root folder of server + root: "/" + //home folder of web - home: "../" + , home: './' //http start //default port of http @@ -947,12 +950,18 @@ var WebSvr = module.exports = function(options) { //get a file var getFile = function(filename, cb) { - var fullpath = path.join(Settings.home, filename); - //if template cache enabled, get from cache pool directly if (Settings.templateCache && templatePool[filename]) { cb && cb(templatePool[filename]); } else { + /* + * webSvr.render('/home.tmpl', model) : means related to Setting.root + * webSvr.render('home.tmpl', model) : means related to Setting.home + */ + var firstChar = filename && filename.charAt(0) + , fullpath = path.join(firstChar == '/' ? Settings.root : Settings.home, filename) + ; + fs.readFile(fullpath, function(err, tmpl) { if (err) { Logger.debug(err); @@ -1013,7 +1022,10 @@ var WebSvr = module.exports = function(options) { if (arguments.length == 1) { model = tmplUrl; - tmplUrl = res.req.url; + /* + * remove the first '/' make it as related path + */ + tmplUrl = res.req.url.substr(1); tmplUrl.indexOf('?') > -1 && (tmplUrl = tmplUrl.substr(0, tmplUrl.indexOf('?'))); } From c71ec1b86395c07a1082c9ac2c0895d8bab7f63e Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Tue, 1 Jul 2014 14:50:53 +0800 Subject: [PATCH 127/195] Update: using empty string '' instead of 'undefined' in url params; --- websvr/websvr.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 328e19b..e537ce0 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -650,17 +650,17 @@ var WebSvr = module.exports = function(options) { for (var i = 0, l = parts.length; i < l; i++) { var part = parts[i] - , url = urls[i + start] + , param = urls[i + start] ; if (part.charAt(0) === ':') { var paramName = part.substr(1); try { - params[paramName] = decodeURIComponent(url) || ''; + params[paramName] = decodeURIComponent(param || ''); } catch(err) { - params[paramName] = url; + params[paramName] = param; } - } else if (part != url) { + } else if (part != param) { return false; } } From 203ee080f6d761a1431cd66cecd5beda2f472f09 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 4 Jul 2014 11:00:13 +0800 Subject: [PATCH 128/195] Update default value of root path; --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index e537ce0..5841c28 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -53,7 +53,7 @@ var WebSvr = module.exports = function(options) { */ var Settings = { //root folder of server - root: "/" + root: process.cwd() //home folder of web , home: './' From 0cd6f79fdff88fbec4d99c99c4909bc075d87a31 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 4 Jul 2014 22:59:54 +0800 Subject: [PATCH 129/195] Add new api: webSvr.use --- websvr/websvr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/websvr/websvr.js b/websvr/websvr.js index e537ce0..09230f7 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1209,6 +1209,7 @@ var WebSvr = module.exports = function(options) { self.parseUrl = Mapper.prototype.parseUrl; //Filter + self.use = Filter.filter; self.filter = Filter.filter; self.file = Filter.file; From a4d74822f85bb62737b937d56f8e989bf71bd4a9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 4 Jul 2014 23:02:08 +0800 Subject: [PATCH 130/195] Update README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c900f3d..5bac491 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,13 @@ Advanced useage: All the request under "test/" will parse the post data and sess } }); + /* + * filter equal to use + */ + webSvr.use('/home', function(req, res) { + //do sth. + req.filter.next(); + }); Handler (HttpHandler, Servlet) -------------- From 4619f6997e09aa59f0382c32cafdf1ac7c5bc2fe Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 5 Jul 2014 16:19:44 +0800 Subject: [PATCH 131/195] Make logger simple; Add debug information; --- websvr/websvr.js | 68 ++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index bb94e87..286c3a6 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -80,9 +80,6 @@ var WebSvr = module.exports = function(options) { , defaultPage: "index.html" , 404: "" - //logger file path - , logger: os.tmpDir() + "/log.txt" - /* Session timeout, in milliseconds. When session is expired, session file will not deleted. @@ -109,42 +106,8 @@ var WebSvr = module.exports = function(options) { Logger: log sth */ var Logger = (function() { - - var lineSeparator = "\r\n", - indentSeparator = "\t", - depth = 9; - - var write = function(logObj, dep) { - var depth = dep || depth, - output = new Date() + lineSeparator; - - function print(pre, obj) { - if (!obj) return; - for (var key in obj) { - var val = obj[key]; - output = output + pre + key + " : " + val + lineSeparator; - if (typeof val == "object") { - (pre.length < depth) && print(pre + indentSeparator, val); - } - } - } - - print(indentSeparator, logObj); - - fs.appendFile(Settings.logger, output, function(err) { - log(err); - }); - }; - /* - Currnetly it's equal to console.log - */ - var log = function() { - console.log.apply(console, arguments); - }; - - /* - Add data before log information + Turn off debug when Settings.debug = false */ var debug = function() { //diable console.log information @@ -152,17 +115,12 @@ var WebSvr = module.exports = function(options) { return; } - var d = new Date().toString(); - - Array.prototype.splice.call(arguments, 0, 0, d.substr(0, d.indexOf(" GMT"))); + var d = new Date().toISOString(); + Array.prototype.splice.call(arguments, 0, 0, d); console.log.apply(console, arguments); }; - return { - log: log - , write: write - , debug: debug - }; + return { debug : debug }; })(); /* @@ -254,7 +212,19 @@ var WebSvr = module.exports = function(options) { if (mapper.session && typeof req.session == "undefined") { req.session = new SessionParser(req, res); } - handler(req, res); + try { + handler(req, res); + } catch(err) { + var errorMsg = [ + '' + , new Date().toISOString() + , 'ErrorUrl: ' + req.url + , err.stack || err.message || 'unknow error' + ].join('\n'); + + console.log(errorMsg); + console.error(errorMsg); + } }; /* @@ -1281,7 +1251,7 @@ var WebSvr = module.exports = function(options) { var httpSvr = http.createServer(requestHandler); httpSvr.listen(port); - Logger.log("Http server running at" + console.log("Http server running at" ,"home:", home ,"port:", port ); @@ -1301,7 +1271,7 @@ var WebSvr = module.exports = function(options) { cert: Settings.httpsCert }, requestHandler).listen(httpsPort); - Logger.log("Https server running at" + console.log("Https server running at" ,"home:", home ,"port:", httpsPort ); From ca2e493348d6eec9d7c5abdf332d15e0e3e39417 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 5 Jul 2014 16:20:42 +0800 Subject: [PATCH 132/195] Update readme; --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 5bac491..2709cc1 100644 --- a/README.md +++ b/README.md @@ -236,9 +236,6 @@ Settings API: //404 template/static file , 404: "404.tmpl" - //logger file path - , logger: os.tmpDir() + "/log.txt" - /* Session timeout, in milliseconds. When session is expired, session file will not deleted. From f2464532e874915e1dbb3683743437b0ac69510f Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 6 Jul 2014 00:02:03 +0800 Subject: [PATCH 133/195] Update API --- README.md | 6 +++--- website/svr/sitetest.js | 8 ++++---- websvr/websvr.js | 33 +++++++++++++++------------------ 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 2709cc1..b85d5b2 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,13 @@ Session based authentication, basically useage: /* General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; + post: parse the post data and stored in req.body; session: init the session and stored in req.session; */ webSvr.filter(function(req, res) { //Link to next filter req.filter.next(); - }, {parse:true, session:true}); + }, {session: true, post: 'json'}); Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" @@ -103,7 +103,7 @@ Handle Login and put the username in Session res.writeHead(401); res.end("Wrong username/password"); } - }, {post: "qs"}); + }, "qs"); //"qs" equal { post: "qs" } or "querystring" Note: diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 9e54e5b..79a1ea4 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -4,7 +4,7 @@ var WebSvr = require("websvr"); //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 var webSvr = new WebSvr({ - root: "./web" + home: "./web" , listDir: true , debug: true }).start(); @@ -20,7 +20,7 @@ webSvr.filter(function(req, res) { //Link to next filter req.filter.next(); -}, {parse:true, session:true}); +}, {post:true, session:true}); /* Session Filter: protect web/* folder => (validation by session); @@ -137,13 +137,13 @@ webSvr //Mapping "hello" to a string, trying at http://localhost:8054/hello .url("hello", "Hello WebSvr!") //Mapping "post" and parse the post in the request, trying at: http://localhost:8054/post.htm - .post("post.htm", function(req, res) { + .url("post.htm", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //With session support: "{session: true}" res.write("You username is " + req.session.get("username")); res.write('
    '); res.end('Received : ' + req.body); - }, {session: true}); + }, {session: true, post: true}); var httpsSvr = new WebSvr({ diff --git a/websvr/websvr.js b/websvr/websvr.js index 286c3a6..1604be1 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -222,7 +222,6 @@ var WebSvr = module.exports = function(options) { , err.stack || err.message || 'unknow error' ].join('\n'); - console.log(errorMsg); console.error(errorMsg); } }; @@ -233,7 +232,7 @@ var WebSvr = module.exports = function(options) { */ var parseBody = function() { //need to parse the request? - if ((mapper.parse || mapper.post) && typeof req.body == "undefined") { + if (mapper.post && typeof req.body == "undefined") { //Must parser the request first, or the post data will lost; BodyParser(req, res, function(data) { var body = data; @@ -546,8 +545,9 @@ var WebSvr = module.exports = function(options) { self.expression = expression; self.handler = handler; - //Has other parameters? - self.extend(options); + typeof options == 'object' + ? self.extend(options) + : (self.post = options); }; Mapper.prototype = { @@ -677,7 +677,15 @@ var WebSvr = module.exports = function(options) { var mapper = new Mapper(expression, handler, options); Filter.filters.push(mapper); - return this; + return self; + } + + //Session: parse the session + , session: function(expression, handler, options) { + this.filter(expression, handler, options); + //Set the previous mapper + Filter.filters[Filter.filters.length - 1].session = true; + return self; } /* @@ -689,7 +697,7 @@ var WebSvr = module.exports = function(options) { //insert at the top of the filter array Filter.filters.splice(0, 0, mapper); - return this; + return self; } }; @@ -754,20 +762,9 @@ var WebSvr = module.exports = function(options) { , url: function(expression, handler, options) { var mapper = new Mapper(expression, handler, options); Handler.handlers.push(mapper); - return self; } - //Post: Parse the post data by default; - , post: function(expression, handler, options) { - return this.url(expression, handler, _.extend({ parse: true }, options)); - } - - //Session: Parse the session and post by default; - , session: function(expression, handler) { - return this.url(expression, handler, { parse: true, session: true }); - } - , handle: function(req, res) { //flag: is matched? for(var i = 0, len = Handler.handlers.length; i < len ; i++) { @@ -1181,6 +1178,7 @@ var WebSvr = module.exports = function(options) { //Filter self.use = Filter.filter; self.filter = Filter.filter; + self.session = Filter.session; self.file = Filter.file; //Handler @@ -1189,7 +1187,6 @@ var WebSvr = module.exports = function(options) { self.handle = Handler.url; self.handler = Handler.url; self.post = Handler.post; - self.session = Handler.session; self.settings = Settings; //Template From 97e1705a9e3e6a39e656213e8568a9196ec0c0d7 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 6 Jul 2014 11:17:11 +0800 Subject: [PATCH 134/195] Update error message; --- websvr/websvr.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 1604be1..bf556f0 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -215,14 +215,16 @@ var WebSvr = module.exports = function(options) { try { handler(req, res); } catch(err) { - var errorMsg = [ - '' - , new Date().toISOString() - , 'ErrorUrl: ' + req.url - , err.stack || err.message || 'unknow error' - ].join('\n'); + var errorMsg + = '\n' + + 'Error ' + new Date().toISOString() + ' ' + req.url + + '\n' + + err.stack || err.message || 'unknow error' + + '\n' + ; console.error(errorMsg); + res.end(errorMsg); } }; From 230f14bb7e6dda1ef63c638642150b66ec08057c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 6 Jul 2014 19:38:59 +0800 Subject: [PATCH 135/195] Update session --- README.md | 7 ------- websvr/websvr.js | 27 +++++++++++++-------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b85d5b2..f7cdb1f 100644 --- a/README.md +++ b/README.md @@ -238,15 +238,8 @@ Settings API: /* Session timeout, in milliseconds. - When session is expired, session file will not deleted. */ , sessionTimeout: 1440000 - /* - Session garbage collection time, in milliseconds. - When session expired time is more than (sessionAge + sessionGCT), - then session file will be unlinked. - */ - , sessionGarbage: 3460000 //session file stored here , sessionDir: os.tmpDir() diff --git a/websvr/websvr.js b/websvr/websvr.js index bf556f0..c77d9df 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -82,18 +82,11 @@ var WebSvr = module.exports = function(options) { /* Session timeout, in milliseconds. - When session is expired, session file will not deleted. */ , sessionTimeout: 1440000 - /* - Session garbage collection time, in milliseconds. - When session expired time is more than (sessionAge + sessionGCT), - then session file will be unlinked. - */ - , sessionGarbage: 3460000 //session file stored here - , sessionDir: os.tmpDir() + , sessionDir: '' //session domain , sessionDomain: '' @@ -340,7 +333,11 @@ var WebSvr = module.exports = function(options) { Init the sessions, load into session pool */ var init = function() { - gcTime = Settings.sessionTimeout + Settings.sessionGarbage; + if (!Settings.sessionDir) { + return; + } + + gcTime = 2 * Settings.sessionTimeout; Logger.debug('Session Dir (gc time):', Settings.sessionDir, gcTime); @@ -401,13 +398,15 @@ var WebSvr = module.exports = function(options) { //force update session in list, convert to big int var update = function(sid, datetime) { - list[sid].__lastAccessTime = +Date.parse(datetime) || +new Date(); + list[sid].__lastAccessTime = datetime + ? +Date.parse(datetime) || +new Date() + : +new Date(); }; //remove a sesson from list var remove = function(sid) { //delete the file - fs.unlink(getPath(sid), function(err) { + Settings.sessionDir && fs.unlink(getPath(sid), function(err) { Logger.debug("unlink session file err", err); }); //remove from list @@ -438,7 +437,7 @@ var WebSvr = module.exports = function(options) { Clean the session in temp folder */ var clean = function() { - fs.readdir(Settings.sessionDir, function(err, files) { + Settings.sessionDir && fs.readdir(Settings.sessionDir, function(err, files) { if (err) return Logger.debug(err); //converted to minutes @@ -470,7 +469,7 @@ var WebSvr = module.exports = function(options) { update(sid, datetime); } else { isValid(sid) && update(sid, datetime); - } + } }; var stop = function() { @@ -507,7 +506,7 @@ var WebSvr = module.exports = function(options) { //force update update(sid); - fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { + Settings.sessionDir && fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { if (err) { Logger.debug(err); } From 325dee24fd9e05fba4a27025524bc6e9025a28fa Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Mon, 7 Jul 2014 09:32:30 +0800 Subject: [PATCH 136/195] Fix bug of SessionManager --- websvr/websvr.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index c77d9df..3622477 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -501,18 +501,19 @@ var WebSvr = module.exports = function(options) { session[key] = val; if (!isValid(sid)) { - return; + return cb && cb(false); } //force update update(sid); - Settings.sessionDir && fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { - if (err) { - Logger.debug(err); - } - - cb && cb(session); - }); + Settings.sessionDir + ? fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { + if (err) { + Logger.debug(err); + } + cb && cb(session); + }) + : cb && cb(session); }; init(); From eb33718275367aede81521331b91405c1bca4a38 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Mon, 7 Jul 2014 09:38:46 +0800 Subject: [PATCH 137/195] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f7cdb1f..d7e0140 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ WebSvr ============== -A simple web server, implement HttpModule(filter) and HttpHandler(servlet), autorecover user session when run into problems. +A simple web server, implement with filter and handler. Features -------------- -- Auto recover: It may run into problems but it can restart and re-covery the user sessions automatically. - Filter (Middleware): A request will try to match all the filters first. - Handler: When a request matched a handler, it will returned, only one handler will be executed. -- Session: Stored in file, with JSON format -- File: Support uploading files -- Cache: Client-cahce is supported. +- Session: By config sessionDir, you can store session in files, with JSON format +- File: Support uploading files +- Cache: Cahce template in release mode Install -------------- From b612b6bdd0c84238ee809f6559cb0988e36d4c4d Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Tue, 8 Jul 2014 14:30:02 +0800 Subject: [PATCH 138/195] update --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 3622477..4ca508b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -217,7 +217,7 @@ var WebSvr = module.exports = function(options) { ; console.error(errorMsg); - res.end(errorMsg); + res.end('
    ' + errorMsg + '
    '); } }; From c4ab17de58cf5124d371f26459b08e8f0064bcc7 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 9 Jul 2014 13:39:40 +0800 Subject: [PATCH 139/195] Add showError API; --- README.md | 2 ++ package.json | 2 +- websvr/websvr.js | 8 ++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d7e0140..5fbf0b3 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,8 @@ Settings API: , defaultPage: "index.html" //404 template/static file , 404: "404.tmpl" + //show errors to user(displayed in response) + , showError: true /* Session timeout, in milliseconds. diff --git a/package.json b/package.json index 2255cd3..97b708c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.18", + "version": "0.1.19", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 4ca508b..5a2ce03 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -73,8 +73,10 @@ var WebSvr = module.exports = function(options) { , cache: true //enable debug information output , debug: true - //enable cache of template/include file (when enabled templates will not be refreshed before restart) + //enable cache of template/include file (when enabled templates will not be refreshed before restart) , templateCache: true + //show errors to user(displayed in response) + , showError: true //default pages, only one is supported , defaultPage: "index.html" @@ -217,7 +219,9 @@ var WebSvr = module.exports = function(options) { ; console.error(errorMsg); - res.end('
    ' + errorMsg + '
    '); + Settings.showError + ? res.end('
    ' + errorMsg + '
    ') + : res.end(); } }; From 12030d6d60f7b7aa815d2a5df80122d4acb7592f Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 11 Jul 2014 11:05:28 +0800 Subject: [PATCH 140/195] Add send and type API; --- README.md | 15 +++++++++++++++ websvr/websvr.js | 26 +++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5fbf0b3..c29061b 100644 --- a/README.md +++ b/README.md @@ -272,6 +272,9 @@ Return request object res.req +Set Content-Type + + res.type('xml'); WebSvr APIs @@ -337,6 +340,18 @@ Parse parameters in url var params = webSvr.parseUrl(expression, reqUrl); +Send API + + webSvr.send([type or statusCode, ] content); + +Send JSON + + webSvr.send('json', { a: 1, b: 2 }); + +Send String + + webSvr.send(401, 'No permission'); + Multi-instance support -------------- diff --git a/websvr/websvr.js b/websvr/websvr.js index 5a2ce03..6760c29 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1141,6 +1141,28 @@ var WebSvr = module.exports = function(options) { res.end(); }; + //set content-type + res.type = function(type) { + res.getHeader('Content-Type') && res.removeHeader("Content-Encoding"); + res.setHeader('Content-Type', mime.lookup(type) || 'text/plain'); + }; + + //Send sth + res.send = function(type, content) { + if (arguments.length < 2) { + content = type; + type = null; + } + + typeof output == 'object' && (output = JSON.stringify(content)); + if (type) { + typeof type == 'number' + ? res.writeHead(type) + : res.type(type); + } + res.end(content || ''); + }; + //render template objects res.render = Template.render; @@ -1169,9 +1191,7 @@ var WebSvr = module.exports = function(options) { return; } - !res.getHeader("Content-Type") - && res.setHeader("Content-Type", mime.lookup(fullPath)); - + res.type(fullPath); res.writeHead(200); res.end(data, "binary"); }); From 2ab680d606b4ce8445304fb71541e91134acb7e3 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 11 Jul 2014 21:30:01 +0800 Subject: [PATCH 141/195] Fix bug; --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 6760c29..66697c0 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1154,7 +1154,7 @@ var WebSvr = module.exports = function(options) { type = null; } - typeof output == 'object' && (output = JSON.stringify(content)); + typeof content == 'object' && (content = JSON.stringify(content)); if (type) { typeof type == 'number' ? res.writeHead(type) From fcc09b4de8b5501de1b1ea2f425caa0b491b9b76 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 13 Jul 2014 21:13:22 +0800 Subject: [PATCH 142/195] Add Cookie Setter --- README.md | 7 +++++++ websvr/websvr.js | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c29061b..7ac65a4 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,13 @@ Set Content-Type res.type('xml'); +Set/Remove Cookie + + //Set Cookie + res.cookie(name, value [, {domain: string, path: string, expires: date}]) + //Remove Cookie + res.cookie(name, null); + WebSvr APIs -------------- diff --git a/websvr/websvr.js b/websvr/websvr.js index 66697c0..c2a6763 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -2,7 +2,7 @@ * Description: websvr * Author: Kris Zhang * Licenses: MIT -* Project url: https://github.com/newghost/node-websvr +* Project url: https://github.com/newghost/websvr */ "use strict"; @@ -161,10 +161,7 @@ var WebSvr = module.exports = function(options) { //Sid doesn't exist, create it if (!sidVal || sidVal.length != 25 || !SessionManager.isValid(sidVal)) { sidVal = SessionManager.create(); - sidStr = " _wsid=" + sidVal + "; path=/"; - Settings.sessionDomain && (sidStr += "; domain=" + Settings.sessionDomain); - - res.setHeader("Set-Cookie", sidStr); + res.cookie('_wsid', sidVal, { domain: Settings.sessionDomain, path: '/'}); }; self.sid = sidVal; @@ -310,6 +307,36 @@ var WebSvr = module.exports = function(options) { parseCookies(); }; + + /* + set: res.cookie(name, value, options) + del: res.cookie(name, null); + */ + var Cookie = function(name, value, options) { + if (arguments.length < 2) { + return Logger.debug('cookie setter ignored', name); + } + + var self = this + , cookies = self.cookies = self.cookies || [] + , setStr = name + '=' + (value || '') + ; + + options = options || {}; + + options.path && (setStr += '; path=' + options.path); + options.domain && (setStr += '; domain=' + options.domain); + + if (value === null) { + setStr += '; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + } else if (options.expires) { + setStr += '; expires=' + (new Date(options.expires)).toGMTString() + } + + cookies.push(setStr); + }; + + /* SessionManager: - Clear expired session files @@ -1115,7 +1142,10 @@ var WebSvr = module.exports = function(options) { res.end = function() { //If Content-Type is undefined, using text/html as default - !res.headersSent && !res.getHeader('Content-Type') && res.setHeader("Content-Type", "text/html"); + if (!res.headersSent) { + !res.getHeader('Content-Type') && res.setHeader('Content-Type', 'text/html'); + res.cookies && res.cookies.length && res.setHeader('Set-Cookie', res.cookies); + } //Execute old end endFn.apply(res, arguments); @@ -1163,6 +1193,8 @@ var WebSvr = module.exports = function(options) { res.end(content || ''); }; + res.cookie = Cookie; + //render template objects res.render = Template.render; From 40da43996ef2de42b3eefcdd266cc89e15eef219 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 13 Jul 2014 22:10:24 +0800 Subject: [PATCH 143/195] update; --- README.md | 2 +- websvr/websvr.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7ac65a4..c318405 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ Set Content-Type Set/Remove Cookie //Set Cookie - res.cookie(name, value [, {domain: string, path: string, expires: date}]) + res.cookie(name, value [, {domain: string, path: string, expires: date, secure, httponly }]) //Remove Cookie res.cookie(name, null); diff --git a/websvr/websvr.js b/websvr/websvr.js index c2a6763..901e17c 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -324,15 +324,17 @@ var WebSvr = module.exports = function(options) { options = options || {}; - options.path && (setStr += '; path=' + options.path); - options.domain && (setStr += '; domain=' + options.domain); - if (value === null) { setStr += '; expires=Thu, 01 Jan 1970 00:00:00 GMT'; } else if (options.expires) { setStr += '; expires=' + (new Date(options.expires)).toGMTString() } + options.path && (setStr += '; path=' + options.path); + options.domain && (setStr += '; domain=' + options.domain); + options.secure && (setStr += '; secure'); + options.httponly && (setStr += '; httponly'); + cookies.push(setStr); }; From 211efc0528a6375161f6b7b5cd1cba2fd825b8b5 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 16 Jul 2014 22:21:32 +0800 Subject: [PATCH 144/195] Update error handling --- websvr/websvr.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 901e17c..e4e6830 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -795,8 +795,12 @@ var WebSvr = module.exports = function(options) { options: object [optional] */ , url: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, options); - Handler.handlers.push(mapper); + if (!expression) { + console.trace('url expression error'); + } else { + var mapper = new Mapper(expression, handler, options); + Handler.handlers.push(mapper); + } return self; } From 380c3efdaf8e5bdd770a8f0ffd5f51ef514e73b3 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 18 Jul 2014 13:56:29 +0800 Subject: [PATCH 145/195] publish to NPM --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97b708c..d464af6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.19", + "version": "0.1.20", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 5c9ab8068add0841e6edb56450ba791326c54d86 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Tue, 29 Jul 2014 18:08:53 +0800 Subject: [PATCH 146/195] update --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index e4e6830..1afa11f 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -796,7 +796,7 @@ var WebSvr = module.exports = function(options) { */ , url: function(expression, handler, options) { if (!expression) { - console.trace('url expression error'); + console.log('url expression ignored'); } else { var mapper = new Mapper(expression, handler, options); Handler.handlers.push(mapper); From b780fea6fb1232289a043287b3519940568a36a0 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 30 Jul 2014 10:40:14 +0800 Subject: [PATCH 147/195] Fix bug --- websvr/websvr.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 1afa11f..9d8f0bb 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1173,7 +1173,8 @@ var WebSvr = module.exports = function(options) { //301/302 : move permanently res.redirect = function(url, status) { - res.writeHead(status ? status : 302, { "Location": url }); + res.statusCode = status || 302; + res.setHeader('Location', url); res.end(); }; From c3c0a209ed7e24dea7b3c0d7b53e77c9807a42c3 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 30 Jul 2014 10:41:07 +0800 Subject: [PATCH 148/195] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d464af6..c47400f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.20", + "version": "0.1.21", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 3fd4ebd1d601f7408f3764c2b19e4219dda822eb Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 14 Aug 2014 17:21:34 +0800 Subject: [PATCH 149/195] Fix bug --- websvr/websvr.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 9d8f0bb..2b4c5f3 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1322,9 +1322,9 @@ var WebSvr = module.exports = function(options) { } //Create https server: Disable by default - if ( options.httpsPort - && options.httpsKey - && options.httpsCert) { + if ( Settings.httpsPort + && Settings.httpsKey + && Settings.httpsCert) { var httpsPort = Settings.httpsPort; From 95276f7716e3a4c097d87513ded2958a10e5db22 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 14 Aug 2014 17:22:02 +0800 Subject: [PATCH 150/195] Version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c47400f..0043523 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.21", + "version": "0.1.22", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 7a92fea88ba7e74a6526f5f6b086cbdbc5477b92 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 15 Aug 2014 14:23:11 +0800 Subject: [PATCH 151/195] Start server by default Update testing scripts --- .gitignore | 1 + package.json | 2 +- website/start.cmd | 11 -------- website/svr/sitetest.js | 26 ++++++++++++------- website/web/header.xml | 1 + website/web/template.node | 3 +-- websvr/websvr.js | 53 +++++++++++++++++++++++++++------------ 7 files changed, 58 insertions(+), 39 deletions(-) create mode 100644 .gitignore delete mode 100644 website/start.cmd diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/package.json b/package.json index 0043523..73537a0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.22", + "version": "0.1.23", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/website/start.cmd b/website/start.cmd deleted file mode 100644 index b3fccc6..0000000 --- a/website/start.cmd +++ /dev/null @@ -1,11 +0,0 @@ -REM start sitetest - -:loop - node svr/sitetest.js - - REM *************************************** - REM Server stop working, restarting... - REM *************************************** - - ping -n 2 127.1>nul -goto loop \ No newline at end of file diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 79a1ea4..64bb72c 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -1,13 +1,18 @@ //import WebSvr module -var WebSvr = require("websvr"); +var WebSvr = require("../../websvr/websvr"); //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 -var webSvr = new WebSvr({ +var webSvr = WebSvr({ home: "./web" , listDir: true , debug: true -}).start(); +}); + + +webSvr.stop(); + +webSvr.start(); /* General filter: parse the post data / session before all request @@ -25,9 +30,9 @@ webSvr.filter(function(req, res) { /* Session Filter: protect web/* folder => (validation by session); */ -webSvr.filter(/web\/[\w\.]+/, function(req, res) { +webSvr.filter(function(req, res) { //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { + if (req.url.indexOf("login.htm") < 0 && req.url.indexOf("login.do") < 0 && req.url !== '/') { //Once session is get initialized //TODO: Make sure next req.session.get() will not load session file again. req.session.get("username", function(val) { @@ -95,7 +100,6 @@ Template: define default template params webSvr.model({ title : "New Page" , username: "kris" - , header : require("fs").readFileSync("web/header.xml") }); /* @@ -146,8 +150,8 @@ webSvr }, {session: true, post: true}); -var httpsSvr = new WebSvr({ - root: "./" +var httpsSvr = WebSvr({ + home: "./web" //disable http server , port: null @@ -165,7 +169,11 @@ var httpsSvr = new WebSvr({ , sessionDir: "tmp/session/" //tempary upload file stored here , uploadDir: "tmp/upload/" -}).start(); +}); + +httpsSvr.stop(); + +httpsSvr.start(); httpsSvr.filters = webSvr.filters; httpsSvr.handlers = webSvr.handlers; \ No newline at end of file diff --git a/website/web/header.xml b/website/web/header.xml index 9c5b982..af2a063 100644 --- a/website/web/header.xml +++ b/website/web/header.xml @@ -1,3 +1,4 @@
    +

    Hello: {{=it.username}}

    This is global header, you can override it in your custom model.

    \ No newline at end of file diff --git a/website/web/template.node b/website/web/template.node index 00b2aba..1015ab4 100644 --- a/website/web/template.node +++ b/website/web/template.node @@ -6,8 +6,7 @@ Login - {{=it.header}} - Hello: {{=it.username}} +
    diff --git a/websvr/websvr.js b/websvr/websvr.js index 2b4c5f3..e41e1f4 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1051,9 +1051,6 @@ var WebSvr = module.exports = function(options) { /*****************Web initial codes*************/ - //Parameters - var home; - var fileHandler = function(req, res) { var url = req.url @@ -1063,7 +1060,7 @@ var WebSvr = module.exports = function(options) { //fs.stat can't recognize the file name with querystring; url = hasQuery > 0 ? url.substring(0, hasQuery) : url; - var fullPath = path.join(home, url); + var fullPath = path.join(Settings.home, url); //Handle path var handlePath = function(phyPath) { @@ -1180,8 +1177,10 @@ var WebSvr = module.exports = function(options) { //set content-type res.type = function(type) { - res.getHeader('Content-Type') && res.removeHeader("Content-Encoding"); - res.setHeader('Content-Type', mime.lookup(type) || 'text/plain'); + if(type && !res.headersSent) { + res.getHeader('Content-Type') && res.removeHeader("Content-Encoding"); + res.setHeader('Content-Type', mime.lookup(type) || 'text/plain'); + } }; //Send sth @@ -1261,12 +1260,12 @@ var WebSvr = module.exports = function(options) { //Get a full path of a request self.getFullPath = function(filePath) { - return path.join(home, filePath); + return path.join(Settings.home, filePath); }; //Write file, filePath is relative path self.writeFile = function(res, filePath, cb, isSvrPath) { - !isSvrPath && (filePath = path.join(home, filePath)); + !isSvrPath && (filePath = path.join(Settings.home, filePath)); fs.exists(filePath, function(exist) { if (exist) { writeFile(res, filePath); @@ -1299,22 +1298,25 @@ var WebSvr = module.exports = function(options) { return self; }; + self.running = false; + //start http server self.start = function() { - //Update the default value of Settings - _.extend(Settings, options); - home = Settings.home; + if (self.running) { + console.log('Already running, ignored'); + return self; + } //Create http server: Enable by default if (Settings.port) { var port = Settings.port; - var httpSvr = http.createServer(requestHandler); + var httpSvr = self.httpSvr || http.createServer(requestHandler); httpSvr.listen(port); console.log("Http server running at" - ,"home:", home + ,"home:", Settings.home ,"port:", port ); @@ -1328,13 +1330,15 @@ var WebSvr = module.exports = function(options) { var httpsPort = Settings.httpsPort; - var httpsSvr = https.createServer({ + var httpsSvr = self.httpsSvr || https.createServer({ key: Settings.httpsKey, cert: Settings.httpsCert - }, requestHandler).listen(httpsPort); + }, requestHandler); + + httpsSvr.listen(httpsPort); console.log("Https server running at" - ,"home:", home + ,"home:", Settings.home ,"port:", httpsPort ); @@ -1347,6 +1351,8 @@ var WebSvr = module.exports = function(options) { //Start session garbage collection SessionManager.start(); + self.running = true; + return self; }; @@ -1356,6 +1362,18 @@ var WebSvr = module.exports = function(options) { self.httpsSvr && self.httpsSvr.close(); SessionManager.stop(); + self.running = false; + + return self; + }; + + //init + self.init = function() { + //Update the default value of Settings + _.extend(Settings, options); + //Start by default + self.start(); + return self; }; @@ -1378,6 +1396,9 @@ var WebSvr = module.exports = function(options) { } }); + //init + self.init(); + return self; }; \ No newline at end of file From 1f3fc29813d4006fc604ccd8effe2074db8b58b6 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 17 Aug 2014 20:03:16 +0800 Subject: [PATCH 152/195] Update API --- README.md | 9 +++++---- websvr/websvr.js | 26 ++++---------------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c318405..f9dcf56 100644 --- a/README.md +++ b/README.md @@ -256,13 +256,14 @@ Response -------------- Extension on reponse object -Ouput file, relative path, relative to the web home +Ouput file, filepath relative to the root - res.writeFile(filePath, [callback]); + res.sendRootFile(filePath, [callback]); -Ouput file, absolute path, relative to the server running +Ouput file, filepath relative to the home (web dir) - res.sendFile(filePath, [callback]); + res.sendFile(filePath); + res.sendHomeFile(filePath); Reidrect request diff --git a/websvr/websvr.js b/websvr/websvr.js index e41e1f4..232a883 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1158,14 +1158,12 @@ var WebSvr = module.exports = function(options) { }; }; - //relative path, relative to the web home - res.writeFile = function(filePath, cb) { - self.writeFile(res, filePath, cb); + res.sendRootFile = function(filePath) { + writeFile(res, path.join(Settings.root, filePath)); }; - //absolute path, relative to the server running - res.sendFile = function(filePath, cb) { - self.writeFile(res, filePath, cb, true) + res.sendHomeFile = res.sendFile = function(filePath) { + writeFile(res, path.join(Settings.home, filePath)); }; //301/302 : move permanently @@ -1263,22 +1261,6 @@ var WebSvr = module.exports = function(options) { return path.join(Settings.home, filePath); }; - //Write file, filePath is relative path - self.writeFile = function(res, filePath, cb, isSvrPath) { - !isSvrPath && (filePath = path.join(Settings.home, filePath)); - fs.exists(filePath, function(exist) { - if (exist) { - writeFile(res, filePath); - cb && cb(exist); - }else{ - //If callback function doesn't exist, write 404 page; - cb ? cb(exist) : self.write404(res); - } - }); - - return self; - }; - self.write403 = function(res) { res.writeHead(403, {"Content-Type": "text/html"}); res.end("Access forbidden!"); From 03ad335b0a9a4e5b1688cbe5757e4179c0c6556c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 17 Aug 2014 20:04:05 +0800 Subject: [PATCH 153/195] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73537a0..f044f90 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.23", + "version": "0.1.24", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 465b3a9cd105ef811a4d7aa481eea824dda7812b Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 17 Aug 2014 20:08:35 +0800 Subject: [PATCH 154/195] Update API --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 232a883..0294b6e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -831,7 +831,7 @@ var WebSvr = module.exports = function(options) { //array: treated it as a file. case "Array": - res.writeFile(handler[0]); + res.sendFile(handler[0]); break; } return true; From a57a94b3ff563e310ac6e4511a4f7b52631dee82 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 6 Sep 2014 16:21:04 +0800 Subject: [PATCH 155/195] Fix bug --- package.json | 2 +- websvr/websvr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f044f90..4f86a6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.24", + "version": "0.1.25", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 0294b6e..8af5382 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1176,7 +1176,7 @@ var WebSvr = module.exports = function(options) { //set content-type res.type = function(type) { if(type && !res.headersSent) { - res.getHeader('Content-Type') && res.removeHeader("Content-Encoding"); + res.getHeader('Content-Type') && res.removeHeader("Content-Type"); res.setHeader('Content-Type', mime.lookup(type) || 'text/plain'); } }; From 726d66adbc073e63afbd9dc6d0b05f4549614c82 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 24 Sep 2014 07:20:06 +0800 Subject: [PATCH 156/195] Add handler.post API --- package.json | 2 +- website/svr/sitetest.js | 18 ++++++++++-------- website/web/login.htm | 22 ++++++++++++++++++++++ websvr/websvr.js | 7 +++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 4f86a6d..75c6807 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.25", + "version": "0.1.26", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 64bb72c..ebf2556 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -9,11 +9,6 @@ var webSvr = WebSvr({ , debug: true }); - -webSvr.stop(); - -webSvr.start(); - /* General filter: parse the post data / session before all request parse: parse the post data and stored in req.body; @@ -150,6 +145,16 @@ webSvr }, {session: true, post: true}); +webSvr.post('post_qs', function(req, res) { + res.end('Received : ' + JSON.stringify(req.body)); +}); + + +webSvr.post('post_json', function(req, res) { + res.end('Received : ' + JSON.stringify(req.body)); +}, 'json'); + + var httpsSvr = WebSvr({ home: "./web" @@ -171,9 +176,6 @@ var httpsSvr = WebSvr({ , uploadDir: "tmp/upload/" }); -httpsSvr.stop(); - -httpsSvr.start(); httpsSvr.filters = webSvr.filters; httpsSvr.handlers = webSvr.handlers; \ No newline at end of file diff --git a/website/web/login.htm b/website/web/login.htm index 6cfc301..b5683fd 100644 --- a/website/web/login.htm +++ b/website/web/login.htm @@ -26,6 +26,28 @@ + +
    + +

    Post querystring Testing:

    +
    +
      +
    • + + + admin +
    • +
    • + + + 12345678 +
    • +
    • + + +
    • +
    +
    diff --git a/websvr/websvr.js b/websvr/websvr.js index 8af5382..f33710e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -804,6 +804,13 @@ var WebSvr = module.exports = function(options) { return self; } + , post: function(expression, handler, options) { + if (expression && handler) { + return this.url(expression, handler, options || 'qs'); + } + return self; + } + , handle: function(req, res) { //flag: is matched? for(var i = 0, len = Handler.handlers.length; i < len ; i++) { From d38ec357fb9b4f5ffade125921005b5e0f3d2b5f Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 9 Oct 2014 15:21:40 +0800 Subject: [PATCH 157/195] Optimize files import --- package.json | 2 +- websvr/websvr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 75c6807..78d0c4d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.26", + "version": "0.1.27", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index f33710e..e65e518 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -951,7 +951,7 @@ var WebSvr = module.exports = function(options) { //Caching of template files. var templatePool = {} - , includeRegExp = //g + , includeRegExp = //g , includeBeginLen = 14 , includeAfterLen = 4 ; From 7113140c4d76837e83dcb53eeee110f474a83faf Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 27 Nov 2014 17:28:26 +0800 Subject: [PATCH 158/195] Use httponly by default --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index e65e518..f89f60e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -161,7 +161,7 @@ var WebSvr = module.exports = function(options) { //Sid doesn't exist, create it if (!sidVal || sidVal.length != 25 || !SessionManager.isValid(sidVal)) { sidVal = SessionManager.create(); - res.cookie('_wsid', sidVal, { domain: Settings.sessionDomain, path: '/'}); + res.cookie('_wsid', sidVal, { domain: Settings.sessionDomain, path: '/', httponly: true }); }; self.sid = sidVal; From 1189a5641b89679dbe88f1f4bed181ebea427c6b Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 27 Nov 2014 17:50:33 +0800 Subject: [PATCH 159/195] Update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78d0c4d..4176db1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.27", + "version": "0.1.28", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 3f828609ee15ca6eb976fce471686e1208813e7d Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 5 Dec 2014 18:05:27 +0800 Subject: [PATCH 160/195] Remove callback in session get/set; --- README.md | 40 ++--- website/svr/sitetest.js | 44 ++--- websvr/websvr.js | 380 +++++++++++++++++----------------------- 3 files changed, 195 insertions(+), 269 deletions(-) diff --git a/README.md b/README.md index f9dcf56..6df7c8c 100644 --- a/README.md +++ b/README.md @@ -49,28 +49,26 @@ Session based authentication, basically useage: Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" - /* - API, pass value of key to callback [optional] - */ - var value = req.session.get(key [, callback]); + + var value = req.session.get(key); /* Session Filter: protect web/* folder => (validation by session); */ - webSvr.filter(/web\/[\w\.]+/, function(req, res) { + webSvr.filter(function(req, res) { //It's not index.htm/login.do, do the session validation - if (req.url.indexOf("index.htm") < 0 && req.url.indexOf("login.do") < 0) { - req.session.get("username", function(val){ - console.log("session", val); + if (req.url.indexOf("login.htm") < 0 && req.url.indexOf("login.do") < 0 && req.url !== '/') { + //Once session is get initialized + //TODO: Make sure next req.session.get() will not load session file again. + var val = req.session.get("username"); - !val && res.end("You must login, first!"); + console.log("session username:", val); + !val && res.end("You must login, first!"); - //Link to next filter - req.filter.next(); - }); + //Link to next filter + req.filter.next(); } else { - //Link to next filter - req.filter.next(); + req.filter.next(); } }); @@ -91,19 +89,19 @@ Handle Login and put the username in Session username: admin password: 12345678 */ - webSvr.handle("login.do", function(req, res) { + webSvr.url("login.do", function(req, res) { var qs = req.body; + console.log(qs); if (qs.username == "admin" && qs.password == "12345678") { //Put key/value pair in session - req.session.set("username", qs.username, function(session) { - res.redirect("/web/setting.htm"); - }); - }else{ + var session = req.session.set("username", qs.username); + console.log(session); + res.redirect("setting.htm"); + } else { res.writeHead(401); res.end("Wrong username/password"); } - }, "qs"); //"qs" equal { post: "qs" } or "querystring" - + }, 'qs'); Note: -------------- diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index ebf2556..f94b3a9 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -7,6 +7,7 @@ var webSvr = WebSvr({ home: "./web" , listDir: true , debug: true + , sessionTimeout: 60 * 1000 }); /* @@ -15,12 +16,11 @@ General filter: parse the post data / session before all request session: init the session and stored in req.session; */ webSvr.filter(function(req, res) { - //TODO: Add greeting words in filter - //res.write("Hello WebSvr!
    "); //Link to next filter req.filter.next(); -}, {post:true, session:true}); + +}, {session:true}); /* Session Filter: protect web/* folder => (validation by session); @@ -30,15 +30,13 @@ webSvr.filter(function(req, res) { if (req.url.indexOf("login.htm") < 0 && req.url.indexOf("login.do") < 0 && req.url !== '/') { //Once session is get initialized //TODO: Make sure next req.session.get() will not load session file again. - req.session.get("username", function(val) { - console.log("session username:", val); - - !val && res.end("You must login, first!"); + var val = req.session.get("username"); - //Link to next filter - req.filter.next(); - }); + console.log("session username:", val); + !val && res.end("You must login, first!"); + //Link to next filter + req.filter.next(); } else { req.filter.next(); } @@ -50,26 +48,19 @@ Handler: login.do => (validate the username & password) username: admin password: 12345678 */ -webSvr.session("login.do", function(req, res) { - var querystring = require("querystring"); - - //TODO: Add an parameter to auto-complete querystring.parse(req.body); - var qs = querystring.parse(req.body); +webSvr.url("login.do", function(req, res) { + var qs = req.body; + console.log(qs); if (qs.username == "admin" && qs.password == "12345678") { //Put key/value pair in session - //TODO: Support put JSON object directly - req.session.set("username", qs.username, function(session) { - //res.writeHead(200, {"Content-Type": "text/html"}); - //res.writeFile("/web/setting.htm"); - //TODO: Error handler of undefined methods - console.log(session); - res.redirect("setting.htm"); - }); + var session = req.session.set("username", qs.username); + console.log(session); + res.redirect("setting.htm"); } else { res.writeHead(401); res.end("Wrong username/password"); } -}); +}, 'qs'); /* Uploader: upload.do => (receive handler) @@ -105,9 +96,8 @@ webSvr.url("template.node", function(req, res) { res.writeHead(200, {"Content-Type": "text/html"}); //render template with session: { "username" : "admin" } - req.session.get(function(session) { - res.render(session); - }); + var session = req.session.get(); + res.render(session); }); /* diff --git a/websvr/websvr.js b/websvr/websvr.js index f89f60e..3173308 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -47,6 +47,8 @@ var WebSvr = module.exports = function(options) { var self = {}; + var SessionStore; + /*****************Web module definitions*************/ /* Configurations @@ -88,7 +90,8 @@ var WebSvr = module.exports = function(options) { , sessionTimeout: 1440000 //session file stored here - , sessionDir: '' + , sessionDir : '' + , sessionStore : '' //session domain , sessionDomain: '' @@ -138,17 +141,16 @@ var WebSvr = module.exports = function(options) { /* Parse request with session support */ - var SessionParser = function(req, res) { - var self = this; + var SessionParser = function() { + var self = this; //session id - self.sid = null; - //init session object - self.init(req, res); + self.sid = null; + self.val = null; }; SessionParser.prototype = { - init: function(req, res) { + init: function(req, res, cb) { var self = this , sidKey = "_wsid" , sidVal @@ -158,36 +160,70 @@ var WebSvr = module.exports = function(options) { //Get or Create sid, sid exist in the cookie, read it var sidVal = req.cookies[sidKey]; - //Sid doesn't exist, create it - if (!sidVal || sidVal.length != 25 || !SessionManager.isValid(sidVal)) { - sidVal = SessionManager.create(); - res.cookie('_wsid', sidVal, { domain: Settings.sessionDomain, path: '/', httponly: true }); + var setSession = function(session) { + self.sid = sidVal; + self.val = session; + self.valid(); + cb && cb(); }; - self.sid = sidVal; - SessionManager.refresh(self.sid); + //Sid doesn't exist, create it + if (!sidVal || sidVal.length != 25) { + sidVal = self.create(); + res.cookie(sidKey, sidVal, { domain: Settings.sessionDomain, path: '/', httponly: true }); + setSession({}); + } else { + SessionStore.get(sidVal, setSession); + } } - //Set an key/value pair in session object - , set: function(key, val, cb) { - var self = this; - SessionManager.set(self.sid, key, val, cb); + //Create a new session id + , create: function() { + /* + * (Time stamp - [random character ...]).length = 25 + */ + var id = (+new Date()).toString(32) + '-'; + for (var i = id.length; i < 25; i++ ) { + id += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ + } + + return id; } - //Get value from session file - , get: function(key, cb) { - var self = this - , val; + , update: function() { + var self = this; + SessionStore.set(self.sid, self.val); + } - //The first parameter is callback function - if (key.constructor == Function) { - cb = key; - key = null; + /* + Does session expired? + If the session is not in the list, add to the list. + i.e. When WebSvr restarted, session will not expired. + */ + , valid: function() { + var self = this; + var isValid = self.val.__lastAccessTime && (+new Date() - self.val.__lastAccessTime <= Settings.sessionTimeout); + if (!isValid) { + SessionStore.del(self.sid); + self.val = {}; } - val = SessionManager.get(self.sid, key); - cb && cb(val); + self.val.__lastAccessTime = +new Date(); + return isValid; + } - return val; + //Set an key/value pair in session object + , set: function(key, val) { + var session = this.val; + session.__lastAccessTime = +new Date(); + session[key] = val; + return session; + } + + //Get value from session file + , get: function(key) { + var session = this.val; + session.__lastAccessTime = +new Date(); + return key ? session[key] : session; } }; @@ -196,16 +232,9 @@ var WebSvr = module.exports = function(options) { */ var Parser = function(req, res, mapper) { - var handler = mapper.handler; - - //add sesion support - var parseSession = function() { - //add sesion support - if (mapper.session && typeof req.session == "undefined") { - req.session = new SessionParser(req, res); - } + var handle = function() { try { - handler(req, res); + mapper.handler(req, res); } catch(err) { var errorMsg = '\n' @@ -222,6 +251,17 @@ var WebSvr = module.exports = function(options) { } }; + //add sesion support + var parseSession = function() { + //add sesion support + if (mapper.session && typeof req.session == "undefined") { + req.session = new SessionParser(); + req.session.init(req, res, handle); + } else { + handle(); + } + }; + /* parse data in request, this should be done before parse session, because session stored in file @@ -247,7 +287,7 @@ var WebSvr = module.exports = function(options) { req.body = body; parseSession(); }); - }else{ + } else { parseSession(); } }; @@ -276,7 +316,7 @@ var WebSvr = module.exports = function(options) { //in fact request will not be parsed again, because body is not undefined parseBody(); }); - }else{ + } else { parseBody(); }; }; @@ -340,121 +380,38 @@ var WebSvr = module.exports = function(options) { /* - SessionManager: + SessionStore Interface (MemoryStore) - Clear expired session files - Valid session + - get : (sid, callback:Session) + - set : (sid, session) + - del : (sid) + * session object, etc: + { + sid: { + .... + __lastAccessTime: dateObject + } + } */ - var SessionManager = (function() { + var MemoryStore = (function() { - //duration time - var gcTime - , timer; - - /* - * session array object, alls: stored sessions and updated time - * etc: - { - sid: { - .... - __lastAccessTime: dateObject - } - } - */ var list = {}; - /* - Init the sessions, load into session pool - */ - var init = function() { - if (!Settings.sessionDir) { - return; - } - - gcTime = 2 * Settings.sessionTimeout; - - Logger.debug('Session Dir (gc time):', Settings.sessionDir, gcTime); - - fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.debug(err); - - //converted to minutes - var expire = (+new Date() - gcTime) / 60000 | 0; - - files.forEach(function(file) { - if (file.length == 25) { - var stamp = parseInt(file.substr(0, file.indexOf('-'))) - , sessionPath = SessionManager.getPath(file); - - if (stamp) { - //remove the expired session - if (stamp < expire) { - remove(file); - } else { - fs.readFile(sessionPath, function(err, data) { - if (err) { - Logger.debug(err); - return; - } - - try { - list[file] = JSON.parse(data); - } catch (e) { - Logger.debug(e); - } - }); - } - } - } - }); - }); - }; - - var getPath = function(sid) { - return path.join(Settings.sessionDir, sid); - }; - - //create a new session id - var create = function() { - /* - * (Time stamp - [random character ...]).length = 25 - */ - var id = (+new Date()).toString('32') + '-'; - for (var i = id.length; i < 25; i++ ) { - id += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ - } - - list[id] = {}; - update(id); - - return id; + //force update session in list, convert to big int + //get session in list, if undefined create new one + var get = function(sid, cb) { + !list[sid] && (list[sid] = {}); + cb && cb(list[sid]); }; - //force update session in list, convert to big int - var update = function(sid, datetime) { - list[sid].__lastAccessTime = datetime - ? +Date.parse(datetime) || +new Date() - : +new Date(); + var set = function(sid, session) { + list[sid] = session; }; //remove a sesson from list - var remove = function(sid) { - //delete the file - Settings.sessionDir && fs.unlink(getPath(sid), function(err) { - Logger.debug("unlink session file err", err); - }); - //remove from list + var del = function(sid) { delete list[sid]; - - Logger.debug("session removed", sid); - }; - - /* - Does session expired? - If the session is not in the list, add to the list. - i.e. When WebSvr restarted, session will not expired. - */ - var isValid = function(sid) { - return list[sid] && ((new Date() - list[sid].__lastAccessTime) || 0 <= Settings.sessionTimeout); }; /* @@ -462,15 +419,33 @@ var WebSvr = module.exports = function(options) { */ var cleanHandler = function() { for (var sid in list) { - !isValid(sid) && remove(sid); + var session = list[sid]; + var isValid = session.__lastAccessTime && ((new Date() - session.__lastAccessTime) || 0 <= Settings.sessionTimeout * 2); + !isValid && del(sid); } }; + setInterval(cleanHandler, Settings.sessionTimeout * 2); + + return { + get : get + , set : set + , del : del + } + + })(); + + var FileStore = (function() { + + var getPath = function(sid) { + return path.join(Settings.sessionDir, sid); + }; + /* - Clean the session in temp folder + Clean the session in session folder */ - var clean = function() { - Settings.sessionDir && fs.readdir(Settings.sessionDir, function(err, files) { + var cleanHandler = function() { + fs.readdir(Settings.sessionDir, function(err, files) { if (err) return Logger.debug(err); //converted to minutes @@ -491,81 +466,46 @@ var WebSvr = module.exports = function(options) { }); }; - /* - * refresh session in list valid first, - * if not expired update the time - * if not exist create new one - */ - var refresh = function(sid, datetime) { - if (!list[sid]) { - list[sid] = {}; - update(sid, datetime); - } else { - isValid(sid) && update(sid, datetime); - } - }; - - var stop = function() { - clearInterval(timer); - timer = null; - }; - - //stop before new session start - var start = function() { - //stop cleanHandler if available - stop(); - //clean the old sessions - clean(); - timer = setInterval(cleanHandler, gcTime); + var del = function(sid) { + fs.unlink(getPath(sid), function(err) { + Logger.debug("unlink session file err", err); + }); }; - var get = function(sid, key) { - var session = list[sid] || {}; - - if (!isValid(sid)) { - return ''; - } - update(sid); - return key ? session[key] : session; + var set = function(sid, session) { + fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { + if (err) { + Logger.debug(err); + } + }); }; - var set = function(sid, key, val, cb) { - var session = list[sid] || {}; - session[key] = val; - - if (!isValid(sid)) { - return cb && cb(false); - } + var get = function(sid, cb) { + var session = {}; + fs.readFile(getPath(sid), function(err, data) { + if (err) { + Logger.debug(err); + cb && cb(session); + return; + } - //force update - update(sid); - Settings.sessionDir - ? fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { - if (err) { - Logger.debug(err); - } - cb && cb(session); - }) - : cb && cb(session); - }; + try { + session = JSON.parse(data); + } catch (e) { + Logger.debug(e); + } + cb && cb(session); + }); + } - init(); + setInterval(cleanHandler, Settings.sessionTimeout * 2); return { - init: init - , list: list - , create: create - , update: update - , remove: remove - , refresh: refresh - , isValid: isValid - , getPath: getPath - , clean: clean - , start: start - , stop: stop - , get: get - , set: set + get : get + , set : set + , del : del } + })(); /* @@ -796,7 +736,7 @@ var WebSvr = module.exports = function(options) { */ , url: function(expression, handler, options) { if (!expression) { - console.log('url expression ignored'); + Logger.log('url expression ignored'); } else { var mapper = new Mapper(expression, handler, options); Handler.handlers.push(mapper); @@ -1163,6 +1103,9 @@ var WebSvr = module.exports = function(options) { res.write = res.writeHead = res.setHeader = function() { Logger.debug("response is already end, response.write ignored!") }; + + //Update session when resonse.end is executed + req.session && req.session.update(); }; res.sendRootFile = function(filePath) { @@ -1334,12 +1277,6 @@ var WebSvr = module.exports = function(options) { self.httpsSvr = httpsSvr; } - /* - init modules - */ - //Start session garbage collection - SessionManager.start(); - self.running = true; return self; @@ -1349,8 +1286,6 @@ var WebSvr = module.exports = function(options) { self.stop = function() { self.httpSvr && self.httpSvr.close(); self.httpsSvr && self.httpsSvr.close(); - SessionManager.stop(); - self.running = false; return self; @@ -1360,6 +1295,9 @@ var WebSvr = module.exports = function(options) { self.init = function() { //Update the default value of Settings _.extend(Settings, options); + + SessionStore = Settings.sessionDir ? FileStore : MemoryStore; + //Start by default self.start(); From f305358143bdabc31cad3250a1243f4858f5b481 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 6 Dec 2014 10:54:11 +0800 Subject: [PATCH 161/195] Update docs and testing site --- README.md | 80 +++++++++++++++++++++-------------------- website/svr/sitetest.js | 40 ++++++++++++++------- website/web/header.xml | 3 +- website/web/login.htm | 3 +- website/web/setting.htm | 3 +- websvr/websvr.js | 16 ++++++--- 6 files changed, 86 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 6df7c8c..c1191c9 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,12 @@ It's simple to start the websvr. //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 - var webSvr = new WebSvr({ - home: "./" + var webSvr = WebSvr({ + home: "./web" , listDir: true , debug: true - }).start(); + , sessionTimeout: 60 * 1000 + }); Filter (HttpModule) @@ -38,19 +39,23 @@ Filter (HttpModule) Session based authentication, basically useage: /* - General filter: parse the post data / session before all request - post: parse the post data and stored in req.body; - session: init the session and stored in req.session; + Session support; */ webSvr.filter(function(req, res) { //Link to next filter req.filter.next(); - }, {session: true, post: 'json'}); + }, {session:true}); -Advanced useage: All the request under "test/" will parse the post data and session by default, except the "index.htm" and "login.do" + /* + * filter equal to use + */ + webSvr.use('/home', function(req, res) { + //do sth. + req.filter.next(); + }); - var value = req.session.get(key); +Filter all the requests that begin with "test/", check the user permission in session, except the "index.htm" and "login.do". /* Session Filter: protect web/* folder => (validation by session); @@ -58,8 +63,6 @@ Advanced useage: All the request under "test/" will parse the post data and sess webSvr.filter(function(req, res) { //It's not index.htm/login.do, do the session validation if (req.url.indexOf("login.htm") < 0 && req.url.indexOf("login.do") < 0 && req.url !== '/') { - //Once session is get initialized - //TODO: Make sure next req.session.get() will not load session file again. var val = req.session.get("username"); console.log("session username:", val); @@ -70,30 +73,23 @@ Advanced useage: All the request under "test/" will parse the post data and sess } else { req.filter.next(); } - }); - - /* - * filter equal to use - */ - webSvr.use('/home', function(req, res) { - //do sth. - req.filter.next(); - }); + }, { session: true }); Handler (HttpHandler, Servlet) -------------- -Handle Login and put the username in Session +Handle Login and put the username in session /* Handler: login.do => (validate the username & password) username: admin password: 12345678 + webSvr.url equal to webSvr.get/webSvr.post/webSvr.handle */ webSvr.url("login.do", function(req, res) { var qs = req.body; console.log(qs); if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session + //Add username in session var session = req.session.set("username", qs.username); console.log(session); res.redirect("setting.htm"); @@ -115,17 +111,17 @@ The result is request: "domain.com/admin/root/login.svr" match: true -Handler : Match from the begining but it can bypass '/', for example: +Handler : Match from the begining (ignore the first '/'), etc: - websvr.handle("root/login", cb) //or + websvr.handle("root/login", cb) //equal to websvr.handle("/root/login", cb) -The result is: +etc: request: "domain.com/root/login.svr" match: true request: "domain.com/admin/root/login.svr" match: false -You can use regular expression to match part of url in Handler. +You can use regular expression to match any part of url in Handler. Cookies -------------- @@ -138,9 +134,9 @@ Template -------------- Render template with params, using doT template engine - res.render([view, ] model); + res.render([view, model]); -View is optional, in this case it will get the template path from req.url +View is optional, it will get the location of template from req.url res.render({json: true}); @@ -163,15 +159,14 @@ for example: webSvr.engine(require("doT").compile); webSvr.engine(require("jade").compile); -You can define some default properties in model, for example header/footer, this parameters will be overridden if they have the same name in your custom model. +You can define some default properties in model, for example header/footer, this parameters will be overridden if they have the same key in your custom model. webSvr.model({ - title : "New Page" - , username: "kris" - , header : require("fs").readFileSync("web/header.xml") + title : "WebSvr Page" + , username: "WebSvr" }); -And more, you can use template and render it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.home; +You can use template and render it by using websvr.render(tmplPath, model, callback), tmplPath relative to webSvr.home; //pre-defined model var model = {}; @@ -184,9 +179,9 @@ And more, you can use template and render it by using websvr.render(tmplPath, mo console.log(model); }); -Include file, you can using "#include" to include a file during rendering a template, in order to make the process easier, the file will fetched from the cache pool so the first refresh will not work after restart the server; +Include file, you can using "#include" to include a file during rendering a template, in order to make the process easier, the file will fetched from the cache pool so the first refresh will not work when you first start the server; -###Be ware: include file path relative to web home, not the template file itself.### +###Be ware: include file: relative to web home, not the template file itself.### @@ -194,9 +189,18 @@ Include file, you can using "#include" to include a file during rendering a temp Cache templates, by default, server will cache the templates(include the "include file" in the templates), turn it off via: - var webSvr = new WebSvr({ + var webSvr = WebSvr({ templateCache: false - }).start(); + }); + + + +Enable template engine and '', using: res.render()/res.render(model)/res.render(tmplPath, model), etc + +webSvr.url(['login.htm', 'setting.htm'], function(req, res) { + res.render(); +}); + Settings -------------- @@ -310,7 +314,7 @@ Post type Handle session - webSvr.session("sessionrequire", function(req, res) { + webSvr.session("session required url", function(req, res) { console.log(req.session); res.end(); }); diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index f94b3a9..8444490 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -1,6 +1,7 @@ //import WebSvr module var WebSvr = require("../../websvr/websvr"); + //Start the WebSvr, runnting at parent folder, default port is 8054, directory browser enabled; //Trying at: http://localhost:8054 var webSvr = WebSvr({ @@ -10,26 +11,22 @@ var webSvr = WebSvr({ , sessionTimeout: 60 * 1000 }); + /* -General filter: parse the post data / session before all request - parse: parse the post data and stored in req.body; - session: init the session and stored in req.session; +Session support; */ webSvr.filter(function(req, res) { - //Link to next filter req.filter.next(); - }, {session:true}); + /* Session Filter: protect web/* folder => (validation by session); */ webSvr.filter(function(req, res) { - //It's not index.htm/login.do, do the session validation + //Not index.htm/login.do, do the session validation if (req.url.indexOf("login.htm") < 0 && req.url.indexOf("login.do") < 0 && req.url !== '/') { - //Once session is get initialized - //TODO: Make sure next req.session.get() will not load session file again. var val = req.session.get("username"); console.log("session username:", val); @@ -40,19 +37,20 @@ webSvr.filter(function(req, res) { } else { req.filter.next(); } -}); +}, { session: true }); /* Handler: login.do => (validate the username & password) username: admin password: 12345678 +webSvr.url equal to webSvr.get/webSvr.post/webSvr.handle */ webSvr.url("login.do", function(req, res) { var qs = req.body; console.log(qs); if (qs.username == "admin" && qs.password == "12345678") { - //Put key/value pair in session + //Add username in session var session = req.session.set("username", qs.username); console.log(session); res.redirect("setting.htm"); @@ -62,6 +60,7 @@ webSvr.url("login.do", function(req, res) { } }, 'qs'); + /* Uploader: upload.do => (receive handler) */ @@ -73,6 +72,7 @@ webSvr.file("upload.do", function(req, res) { res.end(JSON.stringify(req.files)); }); + /* Redirect: redirect request, try at: http://localhost:8054/redirect */ @@ -80,14 +80,24 @@ webSvr.url("redirect", function(req, res) { res.redirect("/svr/websvr.all.js"); }); + /* Template: define default template params */ webSvr.model({ - title : "New Page" - , username: "kris" + title : "WebSvr Page" + , username: "WebSvr" +}); + + +/* +Enable template engine and '', using: res.render()/res.render(model)/res.render(tmplPath, model) +*/ +webSvr.url(['login.htm', 'setting.htm'], function(req, res) { + res.render(); }); + /* Template: render template with params */ @@ -100,6 +110,7 @@ webSvr.url("template.node", function(req, res) { res.render(session); }); + /* Template: render template with jade */ @@ -117,6 +128,7 @@ webSvr.url("template.jade", function(req, res) { }); }); + /* Simple redirect API: */ @@ -145,6 +157,10 @@ webSvr.post('post_json', function(req, res) { }, 'json'); + +/* +* HTTPS server +*/ var httpsSvr = WebSvr({ home: "./web" diff --git a/website/web/header.xml b/website/web/header.xml index af2a063..0bfc6be 100644 --- a/website/web/header.xml +++ b/website/web/header.xml @@ -1,4 +1,3 @@
    -

    Hello: {{=it.username}}

    -

    This is global header, you can override it in your custom model.

    +
    Hello: {{=it.username}}, this is global header, you can override it in your custom model.
    \ No newline at end of file diff --git a/website/web/login.htm b/website/web/login.htm index b5683fd..6f408a8 100644 --- a/website/web/login.htm +++ b/website/web/login.htm @@ -3,10 +3,11 @@ - Login + {{=it.title}} +

    WebSvr configuration login:

      diff --git a/website/web/setting.htm b/website/web/setting.htm index 39ed103..2d4a074 100644 --- a/website/web/setting.htm +++ b/website/web/setting.htm @@ -3,9 +3,10 @@ - Login + {{=it.title}} +
      diff --git a/websvr/websvr.js b/websvr/websvr.js index 3173308..a1152ac 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -16,7 +16,7 @@ var os = require("os"); var http = require("http"); var https = require("https"); -//Open source libraries, some device may not have npm, so reference directly. +//Open source libraries var mime = require("mime"); var formidable = require("formidable"); @@ -937,6 +937,7 @@ var WebSvr = module.exports = function(options) { the first refresh will not work, need some time to update the cache pool */ tmpl = tmpl.replace(includeRegExp, function(fileStr) { + Logger.debug('Include File:', fileStr); var includeFile = fileStr.substring(includeBeginLen, fileStr.length - includeAfterLen); getFile(includeFile); return templatePool[includeFile] || ''; @@ -970,17 +971,22 @@ var WebSvr = module.exports = function(options) { return { //render templates render: function(tmplUrl, model, outFn) { - var res = this, - end = outFn || res.end; + var res = this + , end = outFn || res.end + , len = arguments.length + ; + + len < 1 && (tmplUrl = {}); - if (arguments.length == 1) { + if (len < 2) { model = tmplUrl; /* * remove the first '/' make it as related path */ tmplUrl = res.req.url.substr(1); - tmplUrl.indexOf('?') > -1 && (tmplUrl = tmplUrl.substr(0, tmplUrl.indexOf('?'))); + var idx = tmplUrl.indexOf('?'); + idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); } getTemplate(tmplUrl, function(tmpl) { From 0e2d6c10fd588e8c263e23973865749c3e9d1225 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 6 Dec 2014 14:21:50 +0800 Subject: [PATCH 162/195] Assign a new session id when it expires; Fix FileStore (session stored in files) --- website/svr/sitetest.js | 14 ++++- websvr/websvr.js | 134 +++++++++++++++++++++------------------- 2 files changed, 83 insertions(+), 65 deletions(-) diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 8444490..9dc46a9 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -8,7 +8,7 @@ var webSvr = WebSvr({ home: "./web" , listDir: true , debug: true - , sessionTimeout: 60 * 1000 + , sessionTimeout: 1200 * 1000 }); @@ -94,6 +94,8 @@ webSvr.model({ Enable template engine and '', using: res.render()/res.render(model)/res.render(tmplPath, model) */ webSvr.url(['login.htm', 'setting.htm'], function(req, res) { + //Default template engine + webSvr.engine(require("doT").compile); res.render(); }); @@ -160,6 +162,7 @@ webSvr.post('post_json', function(req, res) { /* * HTTPS server +* session stored in files */ var httpsSvr = WebSvr({ home: "./web" @@ -180,8 +183,15 @@ var httpsSvr = WebSvr({ , sessionDir: "tmp/session/" //tempary upload file stored here , uploadDir: "tmp/upload/" + + //10 seconds + , sessionTimeout: 10 * 1000 }); httpsSvr.filters = webSvr.filters; -httpsSvr.handlers = webSvr.handlers; \ No newline at end of file +httpsSvr.handlers = webSvr.handlers; + + +//Clear session in 10 seconds (Session stored in files) +setInterval(httpsSvr.SessionStore.clear, 10000); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index a1152ac..4c013b6 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -91,7 +91,6 @@ var WebSvr = module.exports = function(options) { //session file stored here , sessionDir : '' - , sessionStore : '' //session domain , sessionDomain: '' @@ -160,34 +159,51 @@ var WebSvr = module.exports = function(options) { //Get or Create sid, sid exist in the cookie, read it var sidVal = req.cookies[sidKey]; - var setSession = function(session) { - self.sid = sidVal; - self.val = session; - self.valid(); + //Does session expired? + var getSession = function(session) { + var isValid = session && session.__lastAccessTime && (+new Date() - session.__lastAccessTime <= Settings.sessionTimeout); + + if (isValid) { + self.sid = sidVal; + self.val = session; + self.val.__lastAccessTime = +new Date(); + cb && cb(); + } else { + SessionStore.del(sidVal); + setSession(); + } + }; + + var setSession = function() { + self.create(); + res.cookie(sidKey, self.sid, { domain: Settings.sessionDomain, path: '/', httponly: true }); cb && cb(); }; //Sid doesn't exist, create it if (!sidVal || sidVal.length != 25) { - sidVal = self.create(); - res.cookie(sidKey, sidVal, { domain: Settings.sessionDomain, path: '/', httponly: true }); - setSession({}); + setSession(); } else { - SessionStore.get(sidVal, setSession); + SessionStore.get(sidVal, getSession); } } //Create a new session id , create: function() { + var self = this; + /* * (Time stamp - [random character ...]).length = 25 */ - var id = (+new Date()).toString(32) + '-'; - for (var i = id.length; i < 25; i++ ) { - id += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ + var sid = (+new Date()).toString(32) + '-'; + for (var i = sid.length; i < 25; i++ ) { + sid += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ } - return id; + self.sid = sid; + self.val = { __lastAccessTime: +new Date() }; + + return self; } , update: function() { @@ -195,22 +211,6 @@ var WebSvr = module.exports = function(options) { SessionStore.set(self.sid, self.val); } - /* - Does session expired? - If the session is not in the list, add to the list. - i.e. When WebSvr restarted, session will not expired. - */ - , valid: function() { - var self = this; - var isValid = self.val.__lastAccessTime && (+new Date() - self.val.__lastAccessTime <= Settings.sessionTimeout); - if (!isValid) { - SessionStore.del(self.sid); - self.val = {}; - } - self.val.__lastAccessTime = +new Date(); - return isValid; - } - //Set an key/value pair in session object , set: function(key, val) { var session = this.val; @@ -367,7 +367,7 @@ var WebSvr = module.exports = function(options) { if (value === null) { setStr += '; expires=Thu, 01 Jan 1970 00:00:00 GMT'; } else if (options.expires) { - setStr += '; expires=' + (new Date(options.expires)).toGMTString() + setStr += '; expires=' + (new Date(options.expires)).toGMTString(); } options.path && (setStr += '; path=' + options.path); @@ -396,11 +396,12 @@ var WebSvr = module.exports = function(options) { */ var MemoryStore = (function() { - var list = {}; + var list; //force update session in list, convert to big int //get session in list, if undefined create new one var get = function(sid, cb) { + !list && init(); !list[sid] && (list[sid] = {}); cb && cb(list[sid]); }; @@ -415,9 +416,9 @@ var WebSvr = module.exports = function(options) { }; /* - Session clean handler + Session clear handler */ - var cleanHandler = function() { + var clearHandler = function() { for (var sid in list) { var session = list[sid]; var isValid = session.__lastAccessTime && ((new Date() - session.__lastAccessTime) || 0 <= Settings.sessionTimeout * 2); @@ -425,7 +426,10 @@ var WebSvr = module.exports = function(options) { } }; - setInterval(cleanHandler, Settings.sessionTimeout * 2); + var init = function() { + list = {}; + setInterval(clearHandler, Settings.sessionTimeout * 4); + }; return { get : get @@ -441,31 +445,6 @@ var WebSvr = module.exports = function(options) { return path.join(Settings.sessionDir, sid); }; - /* - Clean the session in session folder - */ - var cleanHandler = function() { - fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.debug(err); - - //converted to minutes - var expire = (+new Date() - gcTime) / 60000 | 0; - - files.forEach(function(file) { - if (file.length == 25) { - var stamp = parseInt(file.substr(0, file.indexOf('-'))); - - if (stamp) { - //remove the expired session - stamp < expire - ? remove(file) - : Logger.debug("session skipped", file); - } - } - }); - }); - }; - var del = function(sid) { fs.unlink(getPath(sid), function(err) { Logger.debug("unlink session file err", err); @@ -475,7 +454,7 @@ var WebSvr = module.exports = function(options) { var set = function(sid, session) { fs.writeFile(getPath(sid), JSON.stringify(session), function(err) { if (err) { - Logger.debug(err); + Logger.error(err); } }); }; @@ -496,14 +475,38 @@ var WebSvr = module.exports = function(options) { } cb && cb(session); }); - } + }; - setInterval(cleanHandler, Settings.sessionTimeout * 2); + /* + Clear the sessions, you should do it manually somewhere, etc: + setInterval(websvr.SessionStore.clear, 200 * 60 * 1000) + */ + var clear = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return Logger.debug(err); + + //Delete these sessions that created very very long ago + var expire = +new Date() - Settings.sessionTimeout * 24; + + for (var i = 0; i < files.length; i++) { + var file = files[i] + , idx = file.indexOf('-') + ; + + if (file.length == 25 && idx > 0) { + var stamp = parseInt(file.substr(0, idx), 32); + //remove the expired session + stamp && stamp < expire && del(file); + } + } + }); + }; return { get : get , set : set , del : del + , clear : clear } })(); @@ -1302,7 +1305,12 @@ var WebSvr = module.exports = function(options) { //Update the default value of Settings _.extend(Settings, options); - SessionStore = Settings.sessionDir ? FileStore : MemoryStore; + if (!Settings.SessionStore) { + SessionStore = Settings.sessionDir ? FileStore : MemoryStore; + } else { + SessionStore = Settings.SessionStore; + } + self.SessionStore = SessionStore; //Start by default self.start(); From b403eec30cdd00cf92800122b2b70301831af31f Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 6 Dec 2014 17:11:24 +0800 Subject: [PATCH 163/195] Update --- website/svr/sitetest.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 9dc46a9..d4191d7 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -162,7 +162,7 @@ webSvr.post('post_json', function(req, res) { /* * HTTPS server -* session stored in files +* session stored in files£ºsupport for hot restart */ var httpsSvr = WebSvr({ home: "./web" @@ -184,8 +184,6 @@ var httpsSvr = WebSvr({ //tempary upload file stored here , uploadDir: "tmp/upload/" - //10 seconds - , sessionTimeout: 10 * 1000 }); @@ -193,5 +191,5 @@ httpsSvr.filters = webSvr.filters; httpsSvr.handlers = webSvr.handlers; -//Clear session in 10 seconds (Session stored in files) -setInterval(httpsSvr.SessionStore.clear, 10000); \ No newline at end of file +//Clear session (Session stored in files) +setInterval(httpsSvr.SessionStore.clear, 1000000); \ No newline at end of file From 8305f2bd9375d912eae9609eb36e6fe89de4836a Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 6 Dec 2014 19:34:09 +0800 Subject: [PATCH 164/195] Update --- website/svr/sitetest.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index d4191d7..fbcd9e6 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -184,6 +184,9 @@ var httpsSvr = WebSvr({ //tempary upload file stored here , uploadDir: "tmp/upload/" + //5 minutes + , sessionTimeout: 5 * 60 * 1000 + }); From 102afad8d3a9a98beeed6e7b4a904ac09fa74fe1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 6 Dec 2014 21:06:56 +0800 Subject: [PATCH 165/195] update --- websvr/websvr.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 4c013b6..3dd299e 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -381,18 +381,15 @@ var WebSvr = module.exports = function(options) { /* SessionStore Interface (MemoryStore) - - Clear expired session files - - Valid session - - get : (sid, callback:Session) + - get : (sid, callback:session) - set : (sid, session) - del : (sid) - * session object, etc: - { - sid: { - .... - __lastAccessTime: dateObject - } + session object: { + sid: { + .... + __lastAccessTime: dateObject } + } */ var MemoryStore = (function() { From a7465edf39502ca5411ade978c7d7a1f8c903b11 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 7 Dec 2014 14:13:54 +0800 Subject: [PATCH 166/195] Add RedisStore, save session to redis --- README.md | 21 +++++++++ website/svr/redisstore.js | 99 +++++++++++++++++++++++++++++++++++++++ website/svr/sitetest.js | 25 +++++++++- websvr/websvr.js | 20 +++++--- 4 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 website/svr/redisstore.js diff --git a/README.md b/README.md index c1191c9..2d46241 100644 --- a/README.md +++ b/README.md @@ -386,6 +386,27 @@ Do you want to re-use the filters & handlers? httpsSvr.handlers = webSvr.handlers; +Store session in redis +-------------- + + var RedisStore = require('./redisstore'); + + RedisStore.start({ + port: 6379 + , host: 'ourjs.org' + , auth: 'your-password-if-needed' + , select: 0 + }); + + httpsSvr.sessionStore = RedisStore; + + +Clear expired sessions, only 1 refresh timer is needed + + setInterval(RedisStore.clear, 1000000); + + + Lincenses ---- diff --git a/website/svr/redisstore.js b/website/svr/redisstore.js new file mode 100644 index 0000000..268871c --- /dev/null +++ b/website/svr/redisstore.js @@ -0,0 +1,99 @@ +/* +* Description: Session stored in redis for websvr +* Author: Kris Zhang +* Licenses: MIT +* Project url: https://github.com/newghost/websvr-redis +*/ + +var redis = require('redis'); + +var RedisStore = module.exports = (function() { + + var client; + + var del = function(sid) { + client.del(sid); + }; + + var set = function(sid, session) { + client.set(sid, JSON.stringify(session), function(err) { + err && console.error(err); + }); + }; + + var get = function(sid, cb) { + var session = {}; + + client.get(sid, function(err, data) { + if (err) { + console.error(err); + } else { + try { + session = JSON.parse(data); + } catch(e) { + del(sid); + } + } + + cb && cb(session); + }); + }; + + /* + Clear the sessions, you should do it manually somewhere, etc: + setInterval(websvr.SessionStore.clear, 200 * 60 * 1000) + */ + var clear = function() { + client.keys('*', function (err, keys) { + if (err) return console.log(err); + + //Delete these sessions that created very very long ago + var expire = +new Date() - Settings.sessionTimeout * 24; + + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + , idx = key.indexOf('-') + , flag = true + ; + + if (key.length == 25 && idx > 0) { + var stamp = parseInt(key.substr(0, idx), 32); + //expired? + stamp && stamp > expire && (flag = false); + } + + flag && del(key); + } + }); + }; + + var start = function(options) { + options = options || {}; + + var host = options.host || '127.0.0.1' + , port = parseInt(options.host) || 6379 + , opts = options.opts || {} + , auth = options.auth + , idx = options.select || 0 + ; + + client = redis.createClient(port, host, opts); + + auth && client.auth(auth); + + client.select(idx); + + client.on('error', function (err) { + console.error('Error ' + err); + }); + }; + + return { + get : get + , set : set + , del : del + , clear : clear + , start : start + } + +})(); \ No newline at end of file diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index fbcd9e6..442c670 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -189,10 +189,31 @@ var httpsSvr = WebSvr({ }); +httpsSvr.model({ title: 'HTTPS PAGE', username: 'HTTPS' }); httpsSvr.filters = webSvr.filters; httpsSvr.handlers = webSvr.handlers; -//Clear session (Session stored in files) -setInterval(httpsSvr.SessionStore.clear, 1000000); \ No newline at end of file + +/* +* Store your session in redis +* Requires: npm install redis +*/ +/* +var RedisStore = require('./redisstore'); + +RedisStore.start({ + port: 6379 + , host: 'ourjs.org' + , auth: 'your-password-if-needed' + , select: 0 +}); + +httpsSvr.sessionStore = RedisStore; + + +//Clear expired session, only 1 refresh timer is needed +setInterval(RedisStore.clear, 1000000); + +*/ \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 3dd299e..b0d80e8 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1302,12 +1302,7 @@ var WebSvr = module.exports = function(options) { //Update the default value of Settings _.extend(Settings, options); - if (!Settings.SessionStore) { - SessionStore = Settings.sessionDir ? FileStore : MemoryStore; - } else { - SessionStore = Settings.SessionStore; - } - self.SessionStore = SessionStore; + SessionStore = Settings.sessionDir ? FileStore : MemoryStore; //Start by default self.start(); @@ -1334,6 +1329,19 @@ var WebSvr = module.exports = function(options) { } }); + define(self, 'sessionStore', { + get: function() { + return SessionStore; + }, + set: function(sessionStore) { + if (sessionStore && sessionStore.get && sessionStore.set && sessionStore.del) { + SessionStore = sessionStore; + } else { + Logger.debug('Your session storage do not have interface: get/set/del'); + } + } + }); + //init self.init(); From 1f77534f90cc62142b07d41ef2666819d93f23c9 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 7 Dec 2014 14:19:30 +0800 Subject: [PATCH 167/195] update --- website/svr/redisstore.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/svr/redisstore.js b/website/svr/redisstore.js index 268871c..e7e2d70 100644 --- a/website/svr/redisstore.js +++ b/website/svr/redisstore.js @@ -39,6 +39,8 @@ var RedisStore = module.exports = (function() { }); }; + //Delete these sessions that created long long ago (1 day) + var expire = 24 * 3600 * 1000; /* Clear the sessions, you should do it manually somewhere, etc: setInterval(websvr.SessionStore.clear, 200 * 60 * 1000) @@ -47,9 +49,6 @@ var RedisStore = module.exports = (function() { client.keys('*', function (err, keys) { if (err) return console.log(err); - //Delete these sessions that created very very long ago - var expire = +new Date() - Settings.sessionTimeout * 24; - for (var i = 0; i < keys.length; i++) { var key = keys[i] , idx = key.indexOf('-') From 2f5e29d9ef1007dba18a269dc049a22761bf6800 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 7 Dec 2014 19:34:51 +0800 Subject: [PATCH 168/195] Update --- README.md | 3 +- website/svr/redisstore.js | 98 --------------------------------------- website/svr/sitetest.js | 7 ++- 3 files changed, 5 insertions(+), 103 deletions(-) delete mode 100644 website/svr/redisstore.js diff --git a/README.md b/README.md index 2d46241..d7fb88f 100644 --- a/README.md +++ b/README.md @@ -388,8 +388,9 @@ Do you want to re-use the filters & handlers? Store session in redis -------------- +Install: npm install websvr-redis - var RedisStore = require('./redisstore'); + var RedisStore = require('websvr-redis'); RedisStore.start({ port: 6379 diff --git a/website/svr/redisstore.js b/website/svr/redisstore.js deleted file mode 100644 index e7e2d70..0000000 --- a/website/svr/redisstore.js +++ /dev/null @@ -1,98 +0,0 @@ -/* -* Description: Session stored in redis for websvr -* Author: Kris Zhang -* Licenses: MIT -* Project url: https://github.com/newghost/websvr-redis -*/ - -var redis = require('redis'); - -var RedisStore = module.exports = (function() { - - var client; - - var del = function(sid) { - client.del(sid); - }; - - var set = function(sid, session) { - client.set(sid, JSON.stringify(session), function(err) { - err && console.error(err); - }); - }; - - var get = function(sid, cb) { - var session = {}; - - client.get(sid, function(err, data) { - if (err) { - console.error(err); - } else { - try { - session = JSON.parse(data); - } catch(e) { - del(sid); - } - } - - cb && cb(session); - }); - }; - - //Delete these sessions that created long long ago (1 day) - var expire = 24 * 3600 * 1000; - /* - Clear the sessions, you should do it manually somewhere, etc: - setInterval(websvr.SessionStore.clear, 200 * 60 * 1000) - */ - var clear = function() { - client.keys('*', function (err, keys) { - if (err) return console.log(err); - - for (var i = 0; i < keys.length; i++) { - var key = keys[i] - , idx = key.indexOf('-') - , flag = true - ; - - if (key.length == 25 && idx > 0) { - var stamp = parseInt(key.substr(0, idx), 32); - //expired? - stamp && stamp > expire && (flag = false); - } - - flag && del(key); - } - }); - }; - - var start = function(options) { - options = options || {}; - - var host = options.host || '127.0.0.1' - , port = parseInt(options.host) || 6379 - , opts = options.opts || {} - , auth = options.auth - , idx = options.select || 0 - ; - - client = redis.createClient(port, host, opts); - - auth && client.auth(auth); - - client.select(idx); - - client.on('error', function (err) { - console.error('Error ' + err); - }); - }; - - return { - get : get - , set : set - , del : del - , clear : clear - , start : start - } - -})(); \ No newline at end of file diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 442c670..0bcec9a 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -185,7 +185,7 @@ var httpsSvr = WebSvr({ , uploadDir: "tmp/upload/" //5 minutes - , sessionTimeout: 5 * 60 * 1000 + , sessionTimeout: 2 * 3600 * 1000 }); @@ -198,10 +198,10 @@ httpsSvr.handlers = webSvr.handlers; /* * Store your session in redis -* Requires: npm install redis +* Requires: npm install websvr-redis */ /* -var RedisStore = require('./redisstore'); +var RedisStore = require('websvr-redis'); RedisStore.start({ port: 6379 @@ -215,5 +215,4 @@ httpsSvr.sessionStore = RedisStore; //Clear expired session, only 1 refresh timer is needed setInterval(RedisStore.clear, 1000000); - */ \ No newline at end of file From 479c63d44117dd35f7516bd2e32ab48a47044b0c Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 10 Dec 2014 15:14:09 +0800 Subject: [PATCH 169/195] Upgrade version in NPM --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4176db1..86435c2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.28", + "version": "0.1.29", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 122f65fbfff661dbe77d305fad3179a7d69abe80 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 10 Dec 2014 21:20:42 +0800 Subject: [PATCH 170/195] Update --- package.json | 2 +- websvr/websvr.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 86435c2..c9e8247 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.29", + "version": "0.1.30", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index b0d80e8..501e41b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -404,6 +404,7 @@ var WebSvr = module.exports = function(options) { }; var set = function(sid, session) { + !list && init(); list[sid] = session; }; From a4c3df7a66738892aaaf6029fb9ae9bf936e6cf3 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 11 Dec 2014 15:59:37 +0800 Subject: [PATCH 171/195] Add newID --- websvr/websvr.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index b0d80e8..dd4c6f7 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -189,20 +189,24 @@ var WebSvr = module.exports = function(options) { } //Create a new session id - , create: function() { - var self = this; - + , newID: function() { /* * (Time stamp - [random character ...]).length = 25 */ - var sid = (+new Date()).toString(32) + '-'; + var sid = (+new Date()).toString(32) + '-'; + sid += Settings.serverID || ''; for (var i = sid.length; i < 25; i++ ) { sid += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ } - self.sid = sid; - self.val = { __lastAccessTime: +new Date() }; + return sid; + } + //Binding new sid to this session + , create: function() { + var self = this; + self.sid = self.newID(); + self.val = { __lastAccessTime: +new Date() }; return self; } @@ -1193,6 +1197,9 @@ var WebSvr = module.exports = function(options) { //Mapper self.parseUrl = Mapper.prototype.parseUrl; + //Server ID + self.newID = SessionParser.prototype.newID; + //Filter self.use = Filter.filter; self.filter = Filter.filter; From 174595d66a1c42ce5b2c16185e4932905a84bd2c Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 11 Dec 2014 16:00:07 +0800 Subject: [PATCH 172/195] Add version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86435c2..c9e8247 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.29", + "version": "0.1.30", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 4ae3b79482f6a4555ab6c0759ed124a212a4492c Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 17 Dec 2014 14:15:05 +0800 Subject: [PATCH 173/195] Update --- package.json | 2 +- websvr/websvr.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c9e8247..fa37d52 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.30", + "version": "0.1.31", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index dd4c6f7..477e42f 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -408,6 +408,7 @@ var WebSvr = module.exports = function(options) { }; var set = function(sid, session) { + !list && init(); list[sid] = session; }; From d0f9cca0e86d175c3dc1552d38c4d448870b6853 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 28 Dec 2014 16:45:14 +0800 Subject: [PATCH 174/195] Update session ID --- package.json | 2 +- websvr/test/testUUID.js | 10 ++++++++++ websvr/websvr.js | 25 ++++++++++++++++++++----- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index fa37d52..c52c06f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.31", + "version": "0.1.32", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/test/testUUID.js b/websvr/test/testUUID.js index 9c4d378..0090799 100644 --- a/websvr/test/testUUID.js +++ b/websvr/test/testUUID.js @@ -1,6 +1,8 @@ var UUIDjs = require("./lib/uuid"), test = require("./lib/test"); +var websvr = require('../websvr')().stop(); + (function() { // Private array of chars to use var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); @@ -193,4 +195,12 @@ test("Custom session id, with short Date", function() { + ((Math.random() * 0x40000000 | 0)); //Random 2: Used for distinguish the session uuid += '0000000000'.substr(0, 25 - uuid.length); +}); + +test("Test default Session ID", function() { + var newID = websvr.newID(); +}); + +test("Test Session ID with appended chars", function() { + var newID = websvr.newID(3); }); \ No newline at end of file diff --git a/websvr/websvr.js b/websvr/websvr.js index 477e42f..3ee84fa 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -39,6 +39,8 @@ var _ = { //Shortcuts var define = Object.defineProperty; +//Mapping +var CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); /* * Define and Export WebSvr @@ -189,14 +191,27 @@ var WebSvr = module.exports = function(options) { } //Create a new session id - , newID: function() { + /* + * newId() : [Time Stamp]-[serverID][Random Chars] //fixed length + * newID(n) : [serverID][Time Stamp][Random Chars(n)] + */ + , newID: function(appendLen) { /* * (Time stamp - [random character ...]).length = 25 */ - var sid = (+new Date()).toString(32) + '-'; - sid += Settings.serverID || ''; - for (var i = sid.length; i < 25; i++ ) { - sid += String.fromCharCode((Math.random() * 26 | 0) + 97); /* a-z: 0~26; a = 97 */ + var len = CHARS.length; + var sid = (+new Date()).toString(len); + + if (appendLen) { + sid = Settings.serverID || '' + sid; + for (var i = 0; i < appendLen; i++) { + sid += CHARS[Math.random() * len | 0]; + } + } else { + sid = sid + '-' + (Settings.serverID || ''); + for (var i = sid.length; i < 25; i++ ) { + sid += CHARS[Math.random() * len | 0]; + } } return sid; From 29a84f2eb5ff8e474c5c73794d24ea6b033f5685 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 28 Dec 2014 16:52:47 +0800 Subject: [PATCH 175/195] Update --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 3ee84fa..25aa089 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -511,7 +511,7 @@ var WebSvr = module.exports = function(options) { ; if (file.length == 25 && idx > 0) { - var stamp = parseInt(file.substr(0, idx), 32); + var stamp = parseInt(file.substr(0, idx), CHARS.length); //remove the expired session stamp && stamp < expire && del(file); } From 2ce6d6dc16fd801ee376a4cfbd1f5f5cddaf24af Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 28 Dec 2014 21:34:14 +0800 Subject: [PATCH 176/195] Update --- package.json | 2 +- websvr/websvr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c52c06f..92eadb9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.32", + "version": "0.1.33", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 25aa089..9ff7e9d 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -203,7 +203,7 @@ var WebSvr = module.exports = function(options) { var sid = (+new Date()).toString(len); if (appendLen) { - sid = Settings.serverID || '' + sid; + sid = (Settings.serverID || '') + sid; for (var i = 0; i < appendLen; i++) { sid += CHARS[Math.random() * len | 0]; } From 749b198f7eb3ed46750d4a2196948c1b4b4956c1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 3 Jan 2015 13:32:04 +0800 Subject: [PATCH 177/195] Update --- package.json | 2 +- websvr/websvr.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 92eadb9..819028e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.33", + "version": "0.1.34", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 9ff7e9d..f98ec8a 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -999,14 +999,18 @@ var WebSvr = module.exports = function(options) { len < 1 && (tmplUrl = {}); if (len < 2) { - model = tmplUrl; - /* - * remove the first '/' make it as related path - */ - tmplUrl = res.req.url.substr(1); - - var idx = tmplUrl.indexOf('?'); - idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); + if (typeof tmplUrl == 'object') { + model = tmplUrl; + /* + * remove the first '/' make it as related path + */ + tmplUrl = res.req.url.substr(1); + + var idx = tmplUrl.indexOf('?'); + idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); + } else { + model = {}; + } } getTemplate(tmplUrl, function(tmpl) { From 66e568e2f6d984eb85813cf5d2a3724c231ca321 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 18 Jan 2015 08:14:53 +0800 Subject: [PATCH 178/195] Update upload file --- package.json | 2 +- websvr/websvr.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 819028e..adcdcc1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.34", + "version": "0.1.35", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index f98ec8a..9701179 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -688,7 +688,8 @@ var WebSvr = module.exports = function(options) { this filter should be always at the top of the filter list */ , file: function(expression, handler, options) { - var mapper = new Mapper(expression, handler, {file: true}); + var mapper = new Mapper(expression, handler, options); + mapper.file = true; //insert at the top of the filter array Filter.filters.splice(0, 0, mapper); @@ -1007,7 +1008,7 @@ var WebSvr = module.exports = function(options) { tmplUrl = res.req.url.substr(1); var idx = tmplUrl.indexOf('?'); - idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); + idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); } else { model = {}; } From ed5ee314a6c10f6a2de390f15b99998a28a809ce Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 18 Jan 2015 14:18:49 +0800 Subject: [PATCH 179/195] Add 'before' validation before receiving posting data --- README.md | 17 +++++++++++++++++ package.json | 2 +- website/svr/sitetest.js | 8 +++++++- websvr/websvr.js | 35 ++++++++++++++++++++++++----------- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d7fb88f..7f28172 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ Session based authentication, basically useage: req.filter.next(); }); + Filter all the requests that begin with "test/", check the user permission in session, except the "index.htm" and "login.do". /* @@ -330,6 +331,22 @@ Handle upload file, it's a specfic filter res.end(JSON.stringify(req.files)); }); +Valid File beofre receing it + + /* + * Valid request before receiving + */ + webSvr.file("upload.do", function(req, res) { + res.writeHead(200, {"Content-Type": "text/plain"}); + res.send(req.files); + }).before(function(req, res) { + if ((req.headers['content-length'] || 0) > 245760) { + res.send('Posting is too large, should less than 240K') + } else { + return true + } + }); + Multi-Mapping in Handler or Filter webSvr.handle(["about", "help", "welcome"], function(req, res) { diff --git a/package.json b/package.json index adcdcc1..2283b9f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.35", + "version": "0.1.36", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/website/svr/sitetest.js b/website/svr/sitetest.js index 0bcec9a..e508047 100644 --- a/website/svr/sitetest.js +++ b/website/svr/sitetest.js @@ -18,7 +18,7 @@ Session support; webSvr.filter(function(req, res) { //Link to next filter req.filter.next(); -}, {session:true}); +}, {session: true}); /* @@ -70,6 +70,12 @@ webSvr.file("upload.do", function(req, res) { //form fields is stored in req.body res.write(JSON.stringify(req.body)); res.end(JSON.stringify(req.files)); +}).before(function(req, res) { + if ((req.headers['content-length'] || 0) > 245760) { + res.send('Posting is too large, should less than 240K') + } else { + return true + } }); diff --git a/websvr/websvr.js b/websvr/websvr.js index 9701179..2278ae2 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -123,8 +123,8 @@ var WebSvr = module.exports = function(options) { })(); /* - Body parser, parse the data in request body via - when parse complete, execute the callback, with response data; + Body parser, parse the data in request body + when parse complete, execute the callback with response data; */ var BodyParser = function(req, res, callback) { @@ -282,8 +282,7 @@ var WebSvr = module.exports = function(options) { }; /* - parse data in request, this should be done before parse session, - because session stored in file + parse data in request */ var parseBody = function() { //need to parse the request? @@ -315,6 +314,12 @@ var WebSvr = module.exports = function(options) { parse file in request, this should be at the top of the list */ var parseFile = function() { + if (mapper._before && !mapper._before(req, res)) { + Logger.debug('"before" function does not return true, request ended.'); + res.end('This is not a valid request'); + return + } + //Need to parse the file in request? if (mapper.file && typeof req.body == "undefined") { //Must parser the request first, or the post data maybe lost; @@ -644,6 +649,15 @@ var WebSvr = module.exports = function(options) { for(var key in options) { this[key] = options[key] } + }, + + /* + Something need to be done first: i.e: + check the file size and extension before uploading files; + check the content-length before receiving a post json + */ + before: function(func) { + func && (this._before = func) } }; @@ -672,15 +686,14 @@ var WebSvr = module.exports = function(options) { var mapper = new Mapper(expression, handler, options); Filter.filters.push(mapper); - return self; + return mapper; } //Session: parse the session , session: function(expression, handler, options) { - this.filter(expression, handler, options); - //Set the previous mapper - Filter.filters[Filter.filters.length - 1].session = true; - return self; + var mapper = this.filter(expression, handler, options); + mapper.session = true; + return mapper; } /* @@ -690,10 +703,10 @@ var WebSvr = module.exports = function(options) { , file: function(expression, handler, options) { var mapper = new Mapper(expression, handler, options); mapper.file = true; - //insert at the top of the filter array + //insert at the top of the filter list Filter.filters.splice(0, 0, mapper); - return self; + return mapper; } }; From 3af500cb9a3a0146edf3c8cc69986d41bfa893f7 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 25 Feb 2015 18:09:47 +0800 Subject: [PATCH 180/195] Update session manager --- package.json | 2 +- websvr/websvr.js | 51 ++++++++---------------------------------------- 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 2283b9f..183c0f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.36", + "version": "0.1.37", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 2278ae2..8ba148c 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -183,35 +183,26 @@ var WebSvr = module.exports = function(options) { }; //Sid doesn't exist, create it - if (!sidVal || sidVal.length != 25) { + if (!sidVal) { setSession(); } else { SessionStore.get(sidVal, getSession); } } - //Create a new session id /* - * newId() : [Time Stamp]-[serverID][Random Chars] //fixed length - * newID(n) : [serverID][Time Stamp][Random Chars(n)] + * Create a new session id + * newID(n) : [Time Stamp][serverID][Random Chars(n)] */ , newID: function(appendLen) { - /* - * (Time stamp - [random character ...]).length = 25 - */ var len = CHARS.length; var sid = (+new Date()).toString(len); - if (appendLen) { - sid = (Settings.serverID || '') + sid; - for (var i = 0; i < appendLen; i++) { - sid += CHARS[Math.random() * len | 0]; - } - } else { - sid = sid + '-' + (Settings.serverID || ''); - for (var i = sid.length; i < 25; i++ ) { - sid += CHARS[Math.random() * len | 0]; - } + !appendLen && (appendLen = 4); + + sid += Settings.serverID || ''; + for (var i = 0; i < appendLen; i++) { + sid += CHARS[Math.random() * len | 0]; } return sid; @@ -499,36 +490,10 @@ var WebSvr = module.exports = function(options) { }); }; - /* - Clear the sessions, you should do it manually somewhere, etc: - setInterval(websvr.SessionStore.clear, 200 * 60 * 1000) - */ - var clear = function() { - fs.readdir(Settings.sessionDir, function(err, files) { - if (err) return Logger.debug(err); - - //Delete these sessions that created very very long ago - var expire = +new Date() - Settings.sessionTimeout * 24; - - for (var i = 0; i < files.length; i++) { - var file = files[i] - , idx = file.indexOf('-') - ; - - if (file.length == 25 && idx > 0) { - var stamp = parseInt(file.substr(0, idx), CHARS.length); - //remove the expired session - stamp && stamp < expire && del(file); - } - } - }); - }; - return { get : get , set : set , del : del - , clear : clear } })(); From 238b9eaea380686e2d1a73809a60e7dc4947b746 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 26 Feb 2015 09:28:18 +0800 Subject: [PATCH 181/195] Update session ID --- websvr/websvr.js | 49 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 8ba148c..c27e65d 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -97,6 +97,8 @@ var WebSvr = module.exports = function(options) { //session domain , sessionDomain: '' + , sessionLength: 36 + //tempary upload file stored here , uploadDir: os.tmpDir() }; @@ -183,7 +185,7 @@ var WebSvr = module.exports = function(options) { }; //Sid doesn't exist, create it - if (!sidVal) { + if (!sidVal || sidVal.length != Settings.sessionLength) { setSession(); } else { SessionStore.get(sidVal, getSession); @@ -191,18 +193,23 @@ var WebSvr = module.exports = function(options) { } /* - * Create a new session id - * newID(n) : [Time Stamp][serverID][Random Chars(n)] + * newId() : [Time Stamp]-[serverID][Random Chars] //for sessionid, fixed length + * newID(n) : [Time Stamp][serverID][Random Chars(n)] //for userid */ , newID: function(appendLen) { var len = CHARS.length; var sid = (+new Date()).toString(len); - !appendLen && (appendLen = 4); - - sid += Settings.serverID || ''; - for (var i = 0; i < appendLen; i++) { - sid += CHARS[Math.random() * len | 0]; + if (appendLen) { + sid = (Settings.serverID || '') + sid; + for (var i = 0; i < appendLen; i++) { + sid += CHARS[Math.random() * len | 0]; + } + } else { + sid = sid + '-' + (Settings.serverID || ''); + for (var i = sid.length; i < Settings.sessionLength; i++ ) { + sid += CHARS[Math.random() * len | 0]; + } } return sid; @@ -490,10 +497,36 @@ var WebSvr = module.exports = function(options) { }); }; + /* + Clear the sessions, you should do it manually somewhere, etc: + setInterval(websvr.SessionStore.clear, 200 * 60 * 1000) + */ + var clear = function() { + fs.readdir(Settings.sessionDir, function(err, files) { + if (err) return Logger.debug(err); + + //Delete these sessions that created very very long ago + var expire = +new Date() - Settings.sessionTimeout * 24; + + for (var i = 0; i < files.length; i++) { + var file = files[i] + , idx = file.indexOf('-') + ; + + if (file.length == Settings.sessionLength && idx > 0) { + var stamp = parseInt(file.substr(0, idx), CHARS.length); + //remove the expired session + stamp && stamp < expire && del(file); + } + } + }); + }; + return { get : get , set : set , del : del + , clear : clear } })(); From 14268fb08b0c5b87d6518acf295ed2751007c4fb Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Thu, 26 Feb 2015 10:48:28 +0800 Subject: [PATCH 182/195] update user id --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index c27e65d..4515a28 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -201,7 +201,7 @@ var WebSvr = module.exports = function(options) { var sid = (+new Date()).toString(len); if (appendLen) { - sid = (Settings.serverID || '') + sid; + sid += Settings.serverID || ''; for (var i = 0; i < appendLen; i++) { sid += CHARS[Math.random() * len | 0]; } From ddb5f923450d0449944388b55fdfbbadf52bfadd Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 29 Mar 2015 20:40:04 +0800 Subject: [PATCH 183/195] Add clear template cache API --- README.md | 4 ++++ websvr/websvr.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index 7f28172..34c088c 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,10 @@ Cache templates, by default, server will cache the templates(include the "includ templateCache: false }); +Clear the cached templates + + webSvr.clear() + Enable template engine and '', using: res.render()/res.render(model)/res.render(tmplPath, model), etc diff --git a/websvr/websvr.js b/websvr/websvr.js index 4515a28..875a9d5 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1035,6 +1035,11 @@ var WebSvr = module.exports = function(options) { , model: function(_model) { defaultModel = _model; } + , clear: function() { + for (var tmpl in templatePool) { + delete templatePool[tmpl] + } + } } }()); @@ -1250,6 +1255,7 @@ var WebSvr = module.exports = function(options) { self.render = Template.render; self.engine = Template.engine; self.model = Template.model; + self.clear = Template.clear; //Get a full path of a request self.getFullPath = function(filePath) { From d67f81fa7fb9c67dc1628183549d3fa53472c539 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 7 Apr 2015 21:39:11 +0800 Subject: [PATCH 184/195] Add charset support --- README.md | 5 +++++ websvr/websvr.js | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 34c088c..6ab9350 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,11 @@ Set/Remove Cookie res.cookie(name, null); +Change default charset + + res.charset = 'utf-8' + + WebSvr APIs -------------- Mapping url to file, webSvr.url equal to webSvr.handle diff --git a/websvr/websvr.js b/websvr/websvr.js index 875a9d5..dedcad4 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1174,7 +1174,7 @@ var WebSvr = module.exports = function(options) { res.type = function(type) { if(type && !res.headersSent) { res.getHeader('Content-Type') && res.removeHeader("Content-Type"); - res.setHeader('Content-Type', mime.lookup(type) || 'text/plain'); + res.setHeader('Content-Type', (mime.lookup(type) || 'text/plain') + '; charset=' + (res.charset || 'utf-8')); } }; @@ -1185,7 +1185,11 @@ var WebSvr = module.exports = function(options) { type = null; } - typeof content == 'object' && (content = JSON.stringify(content)); + if (typeof content == 'object') { + content = JSON.stringify(content); + type = type || 'json'; + } + if (type) { typeof type == 'number' ? res.writeHead(type) From 36bf95dec9bb7aa8c0360714b2d3ffa59c4826ec Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Thu, 9 Apr 2015 21:31:46 +0800 Subject: [PATCH 185/195] add default charset --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index dedcad4..a93e94d 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1140,7 +1140,7 @@ var WebSvr = module.exports = function(options) { //If Content-Type is undefined, using text/html as default if (!res.headersSent) { - !res.getHeader('Content-Type') && res.setHeader('Content-Type', 'text/html'); + !res.getHeader('Content-Type') && res.setHeader('Content-Type', 'text/html; charset=' + (res.charset || 'utf-8')); res.cookies && res.cookies.length && res.setHeader('Set-Cookie', res.cookies); } From f392b878505cc63f87724baaeb789464d4cf1361 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sat, 30 May 2015 10:07:36 +0800 Subject: [PATCH 186/195] Fix bug --- package.json | 2 +- websvr/websvr.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 183c0f0..146531c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.37", + "version": "0.1.38", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index a93e94d..a688eaf 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -590,7 +590,20 @@ var WebSvr = module.exports = function(options) { //Pure string without params if (expression.indexOf('/:') < 0) { var idx = reqUrl.indexOf(expression); - return isHandler ? (idx == 0 || idx == 1) : (idx > -1); + + /* + fix: url(['/m'], cb) & url(['/more'], cb) will match the same handler + */ + if (isHandler) { + if ((idx == 0 || idx == 1)) { + var lastChar = reqUrl.charAt(idx + expression.length) + return lastChar == '' || lastChar == '/' || lastChar == '?' + } + } else { + return idx > -1; + } + + return false; //Handle and pickup params } else { var params = this.parseUrl(expression, reqUrl); From 3c85774169c36faf1ffda644370e0c750788e6db Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Fri, 24 Jul 2015 11:04:43 +0800 Subject: [PATCH 187/195] Fix bugs --- package.json | 2 +- websvr/websvr.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 146531c..4346c51 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.38", + "version": "0.1.39", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index a688eaf..41cebcd 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -121,7 +121,13 @@ var WebSvr = module.exports = function(options) { console.log.apply(console, arguments); }; - return { debug : debug }; + var error = function() { + var d = new Date().toISOString(); + Array.prototype.splice.call(arguments, 0, 0, d); + console.error.apply(console, arguments); + }; + + return { debug: debug, error: error }; })(); /* @@ -781,7 +787,7 @@ var WebSvr = module.exports = function(options) { */ , url: function(expression, handler, options) { if (!expression) { - Logger.log('url expression ignored'); + Logger.debug('url expression ignored'); } else { var mapper = new Mapper(expression, handler, options); Handler.handlers.push(mapper); From f4a4298a22d59afe166f6f73281d136bfc7f0d5c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 26 Jul 2015 16:41:41 +0800 Subject: [PATCH 188/195] Add renderRaw method --- README.md | 4 ++++ package.json | 2 +- websvr/websvr.js | 39 +++++++++++++++++++++++++-------------- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6ab9350..6fa8b2f 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,10 @@ View is a absolute path, relative to web root //means related to Setting.root res.render("/list.tmpl", {json: true}); +Render raw HTML views + + res.renderRaw(viewContent, model); + You can change template engine, webSvr.engine(engineFunc); diff --git a/package.json b/package.json index 4346c51..a9271fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.39", + "version": "0.1.40", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", diff --git a/websvr/websvr.js b/websvr/websvr.js index 41cebcd..8250a83 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -980,21 +980,25 @@ var WebSvr = module.exports = function(options) { } }; + var getInclude = function(tmpl, cb) { + /* + find and update all the include files, + will get templates from cache for making the process easier, + the first refresh will not work, need some time to update the cache pool + */ + tmpl = tmpl.replace(includeRegExp, function(fileStr) { + Logger.debug('Include File:', fileStr); + var includeFile = fileStr.substring(includeBeginLen, fileStr.length - includeAfterLen); + getFile(includeFile); + return templatePool[includeFile] || ''; + }); + + cb(tmpl); + } + var getTemplate = function(filename, cb) { getFile(filename, function(tmpl) { - /* - find and update all the include files, - will get templates from cache for making the process easier, - the first refresh will not work, need some time to update the cache pool - */ - tmpl = tmpl.replace(includeRegExp, function(fileStr) { - Logger.debug('Include File:', fileStr); - var includeFile = fileStr.substring(includeBeginLen, fileStr.length - includeAfterLen); - getFile(includeFile); - return templatePool[includeFile] || ''; - }); - - cb(tmpl); + getInclude(tmpl, cb) }); }; @@ -1048,6 +1052,12 @@ var WebSvr = module.exports = function(options) { render(tmpl, model, end); }); } + , renderRaw: function(rawTmpl, model, outFn) { + var res = this + getInclude(rawTmpl, function(tmpl) { + render(tmpl, model, outFn || res.end); + }); + } , engine: function(_engineFunc) { engineFunc = _engineFunc; } @@ -1220,7 +1230,8 @@ var WebSvr = module.exports = function(options) { res.cookie = Cookie; //render template objects - res.render = Template.render; + res.render = Template.render; + res.renderRaw = Template.renderRaw; //params in the matched url req.params = {}; From fedd72a101aa6c7f9b9f51bfd135b268d49e6688 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Tue, 28 Jul 2015 22:50:30 +0800 Subject: [PATCH 189/195] Add support of including file in including file --- websvr/websvr.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index 8250a83..d11eeeb 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -971,10 +971,9 @@ var WebSvr = module.exports = function(options) { Logger.debug(err); cb && cb(""); } else { - tmpl = tmpl.toString(); + tmpl = getInclude(tmpl.toString(), cb) templatePool[filename] = tmpl; Logger.debug('update template cache', filename); - cb && cb(tmpl); } }); } @@ -983,25 +982,20 @@ var WebSvr = module.exports = function(options) { var getInclude = function(tmpl, cb) { /* find and update all the include files, - will get templates from cache for making the process easier, - the first refresh will not work, need some time to update the cache pool + will get templates from cache for making the process easier + include file will be enabled at the next refresh */ - tmpl = tmpl.replace(includeRegExp, function(fileStr) { + tmpl = (tmpl || '').replace(includeRegExp, function(fileStr) { Logger.debug('Include File:', fileStr); var includeFile = fileStr.substring(includeBeginLen, fileStr.length - includeAfterLen); getFile(includeFile); return templatePool[includeFile] || ''; }); - cb(tmpl); + cb && cb (tmpl) + return tmpl } - var getTemplate = function(filename, cb) { - getFile(filename, function(tmpl) { - getInclude(tmpl, cb) - }); - }; - //render a file var render = function(chrunk, model, outFn) { var params @@ -1048,7 +1042,7 @@ var WebSvr = module.exports = function(options) { } } - getTemplate(tmplUrl, function(tmpl) { + getFile(tmplUrl, function(tmpl) { render(tmpl, model, end); }); } From be09af3f085b95787a2b369da989871f4d2ede0c Mon Sep 17 00:00:00 2001 From: "Kris, Zhang Chunliang" Date: Wed, 29 Jul 2015 09:03:39 +0800 Subject: [PATCH 190/195] update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6fa8b2f..33356fd 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,9 @@ webSvr.url(['login.htm', 'setting.htm'], function(req, res) { }); +It also support include file in include files, but you need to refresh more times after the first running. + + Settings -------------- Return configuration of current WebSvr instance From 7985e5eeecf64a56d89132bbc548bcb6df37549c Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 2 Aug 2015 13:10:45 +0800 Subject: [PATCH 191/195] Fix bugs in release mode --- websvr/websvr.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index d11eeeb..fc6e9ed 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -942,6 +942,7 @@ var WebSvr = module.exports = function(options) { //Caching of template files. var templatePool = {} + , includeString = '/g , includeBeginLen = 14 , includeAfterLen = 4 @@ -955,8 +956,25 @@ var WebSvr = module.exports = function(options) { //get a file var getFile = function(filename, cb) { //if template cache enabled, get from cache pool directly - if (Settings.templateCache && templatePool[filename]) { - cb && cb(templatePool[filename]); + var cachedTemplate = templatePool[filename] + + var updateCache = function(err, tmpl) { + if (err) { + Logger.debug(err); + cb && cb(""); + } else { + tmpl = getInclude(tmpl.toString(), cb) + templatePool[filename] = tmpl; + Logger.debug('update template cache', filename); + } + }; + + if (Settings.templateCache && cachedTemplate) { + if (cachedTemplate.indexOf(includeString) > -1) { + updateCache(null, cachedTemplate); + } else { + cb && cb(templatePool[filename]); + } } else { /* * webSvr.render('/home.tmpl', model) : means related to Setting.root @@ -966,16 +984,7 @@ var WebSvr = module.exports = function(options) { , fullpath = path.join(firstChar == '/' ? Settings.root : Settings.home, filename) ; - fs.readFile(fullpath, function(err, tmpl) { - if (err) { - Logger.debug(err); - cb && cb(""); - } else { - tmpl = getInclude(tmpl.toString(), cb) - templatePool[filename] = tmpl; - Logger.debug('update template cache', filename); - } - }); + fs.readFile(fullpath, updateCache); } }; @@ -989,7 +998,7 @@ var WebSvr = module.exports = function(options) { Logger.debug('Include File:', fileStr); var includeFile = fileStr.substring(includeBeginLen, fileStr.length - includeAfterLen); getFile(includeFile); - return templatePool[includeFile] || ''; + return templatePool[includeFile] || fileStr; }); cb && cb (tmpl) From 99af4fee57926eae2363167fc24de21a268ea282 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Sun, 25 Oct 2015 09:18:32 +0800 Subject: [PATCH 192/195] Update --- websvr/websvr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index fc6e9ed..d27180b 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -1014,7 +1014,7 @@ var WebSvr = module.exports = function(options) { params = Object.create(defaultModel); _.extend(params, model); } else { - params = model; + params = model || {}; } try { From 91acf80ac8fb082ce8f923435960c8a5d88a4657 Mon Sep 17 00:00:00 2001 From: "Kris, Zhang" Date: Fri, 13 Nov 2015 09:08:01 +0800 Subject: [PATCH 193/195] update --- websvr/websvr.js | 129 ++++++++++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 46 deletions(-) diff --git a/websvr/websvr.js b/websvr/websvr.js index d27180b..7885c44 100644 --- a/websvr/websvr.js +++ b/websvr/websvr.js @@ -250,6 +250,24 @@ var WebSvr = module.exports = function(options) { } }; + + var errorRender = function(err, customErrorMsg, outFn) { + var errorMsg + = (customErrorMsg || '') + + '\n' + + err.stack || err.message || 'unknow error' + + '\n' + ; + + console.error(errorMsg); + + if (outFn) { + Settings.showError + ? outFn('
      ' + errorMsg.replace(//g, '>') + '
      ') + : outFn(); + } + } + /* Parser: Functions that Filter and Handler will be called */ @@ -259,18 +277,7 @@ var WebSvr = module.exports = function(options) { try { mapper.handler(req, res); } catch(err) { - var errorMsg - = '\n' - + 'Error ' + new Date().toISOString() + ' ' + req.url - + '\n' - + err.stack || err.message || 'unknow error' - + '\n' - ; - - console.error(errorMsg); - Settings.showError - ? res.end('
      ' + errorMsg + '
      ') - : res.end(); + errorRender(err, 'Error ' + new Date().toISOString() + ' ' + req.url, res.end) } }; @@ -1003,63 +1010,91 @@ var WebSvr = module.exports = function(options) { cb && cb (tmpl) return tmpl - } - - //render a file - var render = function(chrunk, model, outFn) { - var params - , tmplFn; - - if (defaultModel) { - params = Object.create(defaultModel); - _.extend(params, model); - } else { - params = model || {}; - } - - try { - tmplFn = engineFunc(chrunk); - outFn(tmplFn(params)); - } catch(err) { - Logger.debug(err); - outFn(JSON.stringify(err)); - } }; return { //render templates - render: function(tmplUrl, model, outFn) { + render: function(tmplUrl, _model, outFn, isRawTmpl) { var res = this , end = outFn || res.end , len = arguments.length ; + /* + format + */ len < 1 && (tmplUrl = {}); if (len < 2) { if (typeof tmplUrl == 'object') { - model = tmplUrl; + _model = tmplUrl; /* * remove the first '/' make it as related path */ tmplUrl = res.req.url.substr(1); var idx = tmplUrl.indexOf('?'); - idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); + idx > -1 && (tmplUrl = tmplUrl.substr(0, idx)); } else { - model = {}; + _model = {}; } } - getFile(tmplUrl, function(tmpl) { - render(tmpl, model, end); - }); + /* + merge model + */ + var model = defaultModel + ? Object.create(defaultModel) + : {} + ; + + _.extend(model, res.model); + _.extend(model, _model); + + + /* + response + */ + var render = function(chrunk) { + var tmplFn; + + try { + tmplFn = engineFunc(chrunk); + end(tmplFn(model)); + } catch(err) { + /* + refer: https://code.google.com/p/v8/issues/detail?id=1914 + at eval (eval at (unknown source), :2:8) + */ + var errStack = err.stack || '' + , lineStr = ':' + , idx = errStack.indexOf(lineStr) + , errmsg = res.req.url + '@' + tmplUrl.substr(0, 400) + + if (idx > 0) { + errStack = errStack.substr(idx) + errStack = errStack.substr(0, errStack.indexOf(')')) + var pos = errStack.substring(lineStr.length) + pos = pos.substring(pos.indexOf(':') + 1, errStack.length - 1) + pos = parseInt(pos) + + if (pos) { + var strFn = (tmplFn || '').toString() + var errStr = strFn.substring(pos - 10, pos + 100) + errmsg += ':' + errStack + '\n ' + errStr + } + } + + errorRender(err, errmsg, end) + } + }; + + isRawTmpl + ? getInclude(tmplUrl, render) + : getFile(tmplUrl, render) } , renderRaw: function(rawTmpl, model, outFn) { - var res = this - getInclude(rawTmpl, function(tmpl) { - render(tmpl, model, outFn || res.end); - }); + Template.render(rawTmpl, model, outFn, true) } , engine: function(_engineFunc) { engineFunc = _engineFunc; @@ -1224,7 +1259,7 @@ var WebSvr = module.exports = function(options) { if (type) { typeof type == 'number' - ? res.writeHead(type) + ? (res.statusCode = 404) : res.type(type); } res.end(content || ''); @@ -1237,7 +1272,9 @@ var WebSvr = module.exports = function(options) { res.renderRaw = Template.renderRaw; //params in the matched url - req.params = {}; + req.params = {}; + //default model + res.model = {}; //initial httprequest var filterChain = new FilterChain(function() { From bb1af30b94db1f6bfd6ddf61408cf68954cd2b1a Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Fri, 11 Dec 2015 23:11:52 +0800 Subject: [PATCH 194/195] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a9271fa..a41d3bf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "websvr", "description": "A simple web server, implement http module(filter) and handler(servlet), autorecover persistent session", - "version": "0.1.40", + "version": "0.1.41", "author": "Kris Zhang ", "dependencies": { "dot": "1.0.0", From 48152562b7b135540ff5592ce29811d23367f9e1 Mon Sep 17 00:00:00 2001 From: Kris Zhang Date: Wed, 29 Jun 2022 09:44:19 +0800 Subject: [PATCH 195/195] Update package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a41d3bf..3952422 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.1.41", "author": "Kris Zhang ", "dependencies": { - "dot": "1.0.0", + "dot": "1.1.3", "formidable": "1.0.14", "mime": "1.2.9" }, @@ -17,4 +17,4 @@ ], "repository": "git://github.com/newghost/websvr.git", "main": "./websvr/websvr.js" -} \ No newline at end of file +}