416 lines
14 KiB
JavaScript
416 lines
14 KiB
JavaScript
|
|
var bytes = require('component-bytes.js');
|
|
|
|
var Runtime = java.lang.Runtime;
|
|
var InetSocketAddress = java.net.InetSocketAddress;
|
|
var Executors = java.util.concurrent.Executors;
|
|
var Bytes = Packages.me.hatter.tools.commons.bytes.Bytes;
|
|
var IOUtil = Packages.me.hatter.tools.commons.io.IOUtil;
|
|
var HttpServer = Packages.com.sun.net.httpserver.HttpServer;
|
|
|
|
// {
|
|
// "status": 200,
|
|
// "headers": [[key, value],[key2, value2]],
|
|
// "responseJSON": ...,
|
|
// "responseBytes": ...,
|
|
// "responseText": ...,
|
|
// }
|
|
var writeResponse = (httpExchange, response) => {
|
|
response.status = (response.status == null) ? 200 : response.status;
|
|
var resBs;
|
|
var resStream = null;
|
|
if (response.responseJSON != null) {
|
|
resBs = Bytes.from(JSON.stringify(response.responseJSON)).getBytes();
|
|
} else if (response.responseText != null) {
|
|
resBs = Bytes.from(response.responseText).getBytes();
|
|
} else if (response.responseStream != null) {
|
|
resBs = null;
|
|
resStream = response.responseStream;
|
|
} else {
|
|
resBs = response.responseBytes;
|
|
}
|
|
var responseHeaders = httpExchange.getResponseHeaders();
|
|
if ((response.headers != null) && (response.headers.length > 0)) {
|
|
for (var i = 0; i < response.headers.length; i++) {
|
|
var keyValue = response.headers[i];
|
|
responseHeaders.set(keyValue[0], keyValue[1]);
|
|
}
|
|
} else {
|
|
responseHeaders.set('Content-Type', 'text/plain;charset=UTF-8');
|
|
}
|
|
httpExchange.sendResponseHeaders(response.status, (resBs != null) ? resBs.length : 0);
|
|
var resBody = httpExchange.getResponseBody();
|
|
if (resBs != null) {
|
|
resBody.write(resBs);
|
|
} else {
|
|
IOUtil.copy(response.responseStream, resBody);
|
|
IOUtil.closeQuietly(response.responseStream);
|
|
}
|
|
resBody.close();
|
|
};
|
|
|
|
exports.parseQuery = exports.parseQueryParameterMap = (httpExchange) => {
|
|
var queryParameterMap = {};
|
|
var rawQuery = $STR(httpExchange.getRequestURI().getRawQuery());
|
|
if (rawQuery != null) {
|
|
rawQuery.split('&').forEach((kv) => {
|
|
var indexOfE = kv.indexOf('=');
|
|
var key = '';
|
|
var value = '';
|
|
if (indexOfE >= 0) {
|
|
key = kv.substring(0, indexOfE);
|
|
value = kv.substring(indexOfE + 1);
|
|
} else {
|
|
key = kv;
|
|
}
|
|
queryParameterMap[key] = queryParameterMap[key] || [];
|
|
queryParameterMap[key].push(value);
|
|
});
|
|
}
|
|
return queryParameterMap;
|
|
};
|
|
|
|
var handleDir = (httpExchange, dir, opts) => {
|
|
var sb = [];
|
|
if (opts.handleDir) {
|
|
if (!httpExchange.getRequestURI().getPath().endsWith('/')) {
|
|
return {
|
|
status: 302,
|
|
"headers": [
|
|
["Location", httpExchange.getRequestURI().getPath() + '/'],
|
|
["Content-Type", 'text/html;charset=UTF-8'],
|
|
],
|
|
bytes: new java.lang.String('<html>\n'
|
|
+ '<head>\n'
|
|
+ '<title>Redirecting ...</title>\n'
|
|
+ '<meta http-equiv="refresh" content="0; URL=' + httpExchange.getRequestURI().getPath() + '/' + '" />\n'
|
|
+ '</head>\n'
|
|
+ '<body>\n'
|
|
+ 'Redirect to new location: ' + httpExchange.getRequestURI().getPath() + '/' + '\n'
|
|
+ '</body>\n'
|
|
+ '</html>').getBytes('UTF-8')
|
|
};
|
|
}
|
|
|
|
var files = dir.getAbsoluteFile().listFiles();
|
|
sb.push('<h1>Directory Listing</h1>\n');
|
|
sb.push('<br>\n');
|
|
$EACH(files, (f) => {
|
|
var isDir = f.isDirectory();
|
|
if (isDir) {
|
|
sb.push('<a href="' + f.getName() + '/">[' + f.getName() + ']</a>'
|
|
+ ' <span style="font-size:12px;"><i>' + new Date(f.lastModified()) + '</i></span>'
|
|
+ '<br>\n');
|
|
} else {
|
|
sb.push('<a href="' + f.getName() + '">' + f.getName() + '</a>'
|
|
+ ' <span style="font-size:12px;">'
|
|
+ bytes.showBytes(f.length())
|
|
+ ' <i>' + new Date(f.lastModified()) + '</i>'
|
|
+ '</span>'
|
|
+ '<br>\n');
|
|
}
|
|
});
|
|
} else {
|
|
sb.push('Directory listing is forbidden.');
|
|
}
|
|
return { html: sb.join('') };
|
|
};
|
|
|
|
// https://www.freeformatter.com/mime-types-list.html
|
|
var mimeTypeList = [
|
|
[['txt', 'text'], 'text/plain;charset=UTF-8'],
|
|
[['jpg', 'jpeg'], 'image/jpeg'],
|
|
[['png'], 'image/png'],
|
|
[['swf'], 'application/x-shockwave-flash'],
|
|
[['js'], 'text/javascript;charset=UTF-8'],
|
|
[['htm', 'html'], 'text/html;charset=UTF-8'],
|
|
[['css'], 'text/css;charset=UTF-8'],
|
|
[['geojson'], 'application/vnd.geo+json'],
|
|
[['svg'], 'image/svg+xml'],
|
|
[['7z'], 'application/x-7z-compressed'],
|
|
[['ace'], 'application/x-ace-compressed'],
|
|
[['acc'], 'application/vnd.americandynamics.acc'],
|
|
[['pdf'], 'application/pdf'],
|
|
[['aac'], 'audio/x-aac'],
|
|
[['apk'], 'application/vnd.android.package-archive'],
|
|
[['dmg'], 'application/x-apple-diskimage'],
|
|
[['mpkg'], 'application/vnd.apple.installer+xml'],
|
|
[['s'], 'text/x-asm'],
|
|
[['avi'], 'video/x-msvideo'],
|
|
[['bin'], 'application/octet-stream'],
|
|
[['bmp'], 'image/bmp'],
|
|
[['torrent'], 'application/x-bittorrent'],
|
|
[['sh'], 'application/x-sh'],
|
|
[['bz'], 'application/x-bzip'],
|
|
[['bz2'], 'application/x-bzip2'],
|
|
[['csh'], 'application/x-csh'],
|
|
[['c'], 'text/x-c'],
|
|
[['csv'], ' text/csv'],
|
|
[['deb'], 'application/x-debian-package'],
|
|
[['dtd'], 'application/xml-dtd'],
|
|
[['dwg'], 'image/vnd.dwg'],
|
|
[['es'], 'application/ecmascript'],
|
|
[['epub'], 'application/epub+zip'],
|
|
[['eml'], 'message/rfc822'],
|
|
[['f4v'], 'video/x-f4v'],
|
|
[['flv'], 'video/x-flv'],
|
|
[['gif'], 'image/gif'],
|
|
[['h261'], 'video/h261'],
|
|
[['h263'], 'video/h263'],
|
|
[['h264'], 'video/h264'],
|
|
[['ico'], 'image/x-icon'],
|
|
[['cer'], 'application/pkix-cert'],
|
|
[['pki'], 'application/pkixcmp'],
|
|
[['crl'], 'application/pkix-crl'],
|
|
[['jad'], 'text/vnd.sun.j2me.app-descriptor'],
|
|
[['jar'], 'application/java-archive'],
|
|
[['class'], 'application/java-vm'],
|
|
[['jnlp'], 'application/x-java-jnlp-file'],
|
|
[['ser'], 'application/java-serialized-object'],
|
|
[['java'], 'text/x-java-source,java'],
|
|
[['json'], 'application/json'],
|
|
[['m3u', 'm3u8'], 'audio/x-mpegurl'],
|
|
[['mathml'], 'application/mathml+xml'],
|
|
[['exe'], 'application/x-msdownload'],
|
|
[['cab'], 'application/vnd.ms-cab-compressed'],
|
|
[['eot'], 'application/vnd.ms-fontobject'],
|
|
[['xls'], 'application/vnd.ms-excel'],
|
|
[['ppt'], 'application/vnd.ms-powerpoint'],
|
|
[['doc'], 'application/msword'],
|
|
[['vsd'], 'application/vnd.visio'],
|
|
[['chm'], 'application/vnd.ms-htmlhelp'],
|
|
[['pptx'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
|
|
[['ppsx'], 'application/vnd.openxmlformats-officedocument.presentationml.slideshow'],
|
|
[['xlsx'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
[['docx'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|
[['dotx'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.template'],
|
|
[['vsdx'], 'application/vnd.visio2013'],
|
|
[['mpeg'], 'video/mpeg'],
|
|
[['mp4a'], 'audio/mp4'],
|
|
[['mp4'], 'video/mp4'],
|
|
[['psd'], 'image/vnd.adobe.photoshop'],
|
|
[['p10'], 'application/pkcs10'],
|
|
[['p12'], 'application/x-pkcs12'],
|
|
[['p7m'], 'application/pkcs7-mime'],
|
|
[['p7s'], 'application/pkcs7-signature'],
|
|
[['p7r'], 'application/x-pkcs7-certreqresp'],
|
|
[['p7b'], 'application/x-pkcs7-certificates'],
|
|
[['p8'], 'application/pkcs8'],
|
|
[['pgp'], 'application/pgp-encrypted'],
|
|
[['rar'], 'application/x-rar-compressed'],
|
|
[['rm'], 'application/vnd.rn-realmedia'],
|
|
[['rtf'], 'application/rtf'],
|
|
[['movie'], 'video/x-sgi-movie'],
|
|
[['tiff'], 'image/tiff'],
|
|
[['tar'], 'application/x-tar'],
|
|
[['tex'], 'application/x-tex'],
|
|
[['ttf'], 'application/x-font-ttf'],
|
|
[['vcs'], 'text/x-vcalendar'],
|
|
[['vcf'], 'text/x-vcard'],
|
|
[['wav'], 'audio/x-wav'],
|
|
[['woff'], 'application/x-font-woff'],
|
|
[['webp'], 'image/webp'],
|
|
[['der'], 'application/x-x509-ca-cert'],
|
|
[['xml'], 'application/xml'],
|
|
[['zip'], 'application/zip']
|
|
];
|
|
|
|
var mimeTypeMap = {};
|
|
mimeTypeList.forEach((m) => {
|
|
m[0].forEach((t) => {
|
|
mimeTypeMap[t] = m[1];
|
|
});
|
|
});
|
|
|
|
var getMimeTypeByExt = (ext) => {
|
|
return mimeTypeMap[ext] || 'application/octet-stream';
|
|
};
|
|
|
|
var fileCacheSize = 0;
|
|
var fileCacheCount = 0;
|
|
var fileCacheMap = {};
|
|
var handleFile = (httpExchange, file, opts) => {
|
|
var fileAbsPath = file.getAbsolutePath();
|
|
if (opts.enableFileCache) {
|
|
println('[INFO] ' + getDateYmd() + ' ' + 'File cache hit: ' + fileAbsPath);
|
|
var cachedFileContent = fileCacheMap[fileAbsPath];
|
|
if (cachedFileContent) { return cachedFileContent; } // cache hit
|
|
}
|
|
if (file.exists()) {
|
|
var path = $STR(httpExchange.getRequestURI().getPath()).toLowerCase();
|
|
var lastIndexOfDot = path.lastIndexOf('.');
|
|
var ext = '';
|
|
if (lastIndexOfDot >= 0) {
|
|
ext = path.substring(lastIndexOfDot + 1);
|
|
}
|
|
var contentType = null;
|
|
if (opts.getContentType) {
|
|
contentType = opts.getContentType(httpExchange, file, ext);
|
|
}
|
|
contentType = contentType || getMimeTypeByExt(ext);
|
|
|
|
if (opts.enableFileCache && (file.length() < (opts.cacheFileLength || (1024 * 1024 * 5)))) { // default cache file length 5MB
|
|
if (fileCacheCount < (opts.cacheCount || 1000)) { // default cache size 1000
|
|
var fileContent = {
|
|
headers: [
|
|
['Content-Type', contentType],
|
|
['Content-Length', file.length()]
|
|
],
|
|
bytes: $$.rFile(file).rStream().bytesAndClose()
|
|
};
|
|
fileCacheCount++;
|
|
fileCacheSize += file.length();
|
|
println('[INFO] ' + getDateYmd() + ' ' + 'File cached: ' + fileAbsPath + ', size: ' + bytes.showBytes(file.length()) + ', cached count: ' + fileCacheCount + ', cached size: ' + bytes.showBytes(fileCacheSize));
|
|
fileCacheMap[fileAbsPath] = fileContent;
|
|
return fileContent;
|
|
}
|
|
}
|
|
return {
|
|
headers: [
|
|
['Content-Type', contentType],
|
|
['Content-Length', file.length()]
|
|
],
|
|
stream: $$.rFile(file).rStream().stream()
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
var getDateYmd = () => {
|
|
return new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(new java.util.Date());
|
|
};
|
|
|
|
/**
|
|
* @param {*} httpExchange
|
|
* @param {*} options
|
|
* basePath - base path
|
|
* getContentType (httpExchange, file, ext) - get content type
|
|
* handleDir - enable dir listing
|
|
*/
|
|
exports.handleFile = (httpExchange, options) => {
|
|
var opts = options || {};
|
|
var path = $STR(httpExchange.getRequestURI().getPath());
|
|
if (path == '/') {
|
|
return handleDir(httpExchange, opts.basePath ? $$.file(opts.basePath) : $$.file(''), opts);
|
|
}
|
|
var f = path.substring(1);
|
|
var file = opts.basePath ? $$.file(opts.basePath, f) : $$.file(f);
|
|
if (file.exists()) {
|
|
if (file.isDirectory()) {
|
|
return handleDir(httpExchange, file, opts);
|
|
} else {
|
|
return handleFile(httpExchange, file, opts);
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {*} port port number
|
|
* @param {*} handler process handle
|
|
* return {
|
|
* text: 'text'
|
|
* }
|
|
* return {
|
|
* html: 'html'
|
|
* }
|
|
* ...
|
|
* @param {*} options
|
|
* processors - processor count
|
|
* printErrors - print exception detail
|
|
*/
|
|
exports.serveHTTP = (port, handler, options) => {
|
|
var opts = options || {};
|
|
var addr = new InetSocketAddress(port);
|
|
var httpServer = HttpServer.create(addr, 0);
|
|
if (opts.processors) {
|
|
httpServer.setExecutor(Executors.newFixedThreadPool(opts.processors));
|
|
} else {
|
|
httpServer.setExecutor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2));
|
|
}
|
|
httpServer.createContext("/", (httpExchange) => {
|
|
try {
|
|
println('[INFO] ' + getDateYmd() + ' ' + httpExchange.getRequestURI());
|
|
var response = handler(httpExchange);
|
|
if (response != null) {
|
|
if (response.text != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": [
|
|
["Content-Type", "text/plain;charset=UTF-8"],
|
|
],
|
|
"responseText": response.text
|
|
};
|
|
} else if (response.html != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": [
|
|
["Content-Type", "text/html;charset=UTF-8"],
|
|
],
|
|
"responseText": response.html
|
|
};
|
|
} else if (response.css != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": [
|
|
["Content-Type", "text/css;charset=UTF-8"],
|
|
],
|
|
"responseText": response.css
|
|
};
|
|
} else if (response.js != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": [
|
|
["Content-Type", "text/javascript;charset=UTF-8"],
|
|
],
|
|
"responseText": response.js
|
|
};
|
|
} else if (response.json != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": [
|
|
["Content-Type", "application/json;charset=UTF-8"],
|
|
],
|
|
"responseJSON": response.json
|
|
};
|
|
} else if (response.bytes != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": response.headers,
|
|
"responseBytes": response.bytes
|
|
};
|
|
} else if (response.stream != null) {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": response.headers,
|
|
"responseStream": response.stream
|
|
};
|
|
} else {
|
|
responseResult = {
|
|
"status": response.status || 200,
|
|
"headers": [
|
|
["Content-Type", "text/plain;charset=UTF-8"],
|
|
],
|
|
"responseText": $STR(response)
|
|
};
|
|
}
|
|
writeResponse(httpExchange, responseResult);
|
|
}
|
|
} catch (e) {
|
|
var errMsg = '[ERROR] Request: ' + httpExchange.getRequestURI() + ', exception: ' + e;
|
|
println('[ERROR] ' + getDateYmd() + ' ' + errMsg);
|
|
if (e.printStackTrace) { e.printStackTrace(); }
|
|
|
|
writeResponse(httpExchange, {
|
|
"status": 500,
|
|
"responseText": opts.printErrors ? errMsg : '500 - Server Error'
|
|
});
|
|
}
|
|
});
|
|
println('[INFO] ' + getDateYmd() + ' ' + "Start listen at: " + port + ' ...');
|
|
httpServer.start();
|
|
};
|
|
|