Serve.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /**
  2. * Copyright 2013-2022 the PM2 project authors. All rights reserved.
  3. * Use of this source code is governed by a license that
  4. * can be found in the LICENSE file.
  5. */
  6. 'use strict';
  7. var fs = require('fs');
  8. var http = require('http');
  9. var url = require('url');
  10. var path = require('path');
  11. var debug = require('debug')('pm2:serve');
  12. var probe = require('@pm2/io');
  13. var errorMeter = probe.meter({
  14. name : '404/sec',
  15. samples : 1,
  16. timeframe : 60
  17. })
  18. /**
  19. * list of supported content types.
  20. */
  21. var contentTypes = {
  22. '3gp': 'video/3gpp',
  23. 'a': 'application/octet-stream',
  24. 'ai': 'application/postscript',
  25. 'aif': 'audio/x-aiff',
  26. 'aiff': 'audio/x-aiff',
  27. 'asc': 'application/pgp-signature',
  28. 'asf': 'video/x-ms-asf',
  29. 'asm': 'text/x-asm',
  30. 'asx': 'video/x-ms-asf',
  31. 'atom': 'application/atom+xml',
  32. 'au': 'audio/basic',
  33. 'avi': 'video/x-msvideo',
  34. 'bat': 'application/x-msdownload',
  35. 'bin': 'application/octet-stream',
  36. 'bmp': 'image/bmp',
  37. 'bz2': 'application/x-bzip2',
  38. 'c': 'text/x-c',
  39. 'cab': 'application/vnd.ms-cab-compressed',
  40. 'cc': 'text/x-c',
  41. 'chm': 'application/vnd.ms-htmlhelp',
  42. 'class': 'application/octet-stream',
  43. 'com': 'application/x-msdownload',
  44. 'conf': 'text/plain',
  45. 'cpp': 'text/x-c',
  46. 'crt': 'application/x-x509-ca-cert',
  47. 'css': 'text/css',
  48. 'csv': 'text/csv',
  49. 'cxx': 'text/x-c',
  50. 'deb': 'application/x-debian-package',
  51. 'der': 'application/x-x509-ca-cert',
  52. 'diff': 'text/x-diff',
  53. 'djv': 'image/vnd.djvu',
  54. 'djvu': 'image/vnd.djvu',
  55. 'dll': 'application/x-msdownload',
  56. 'dmg': 'application/octet-stream',
  57. 'doc': 'application/msword',
  58. 'dot': 'application/msword',
  59. 'dtd': 'application/xml-dtd',
  60. 'dvi': 'application/x-dvi',
  61. 'ear': 'application/java-archive',
  62. 'eml': 'message/rfc822',
  63. 'eps': 'application/postscript',
  64. 'exe': 'application/x-msdownload',
  65. 'f': 'text/x-fortran',
  66. 'f77': 'text/x-fortran',
  67. 'f90': 'text/x-fortran',
  68. 'flv': 'video/x-flv',
  69. 'for': 'text/x-fortran',
  70. 'gem': 'application/octet-stream',
  71. 'gemspec': 'text/x-script.ruby',
  72. 'gif': 'image/gif',
  73. 'gz': 'application/x-gzip',
  74. 'h': 'text/x-c',
  75. 'hh': 'text/x-c',
  76. 'htm': 'text/html',
  77. 'html': 'text/html',
  78. 'ico': 'image/vnd.microsoft.icon',
  79. 'ics': 'text/calendar',
  80. 'ifb': 'text/calendar',
  81. 'iso': 'application/octet-stream',
  82. 'jar': 'application/java-archive',
  83. 'java': 'text/x-java-source',
  84. 'jnlp': 'application/x-java-jnlp-file',
  85. 'jpeg': 'image/jpeg',
  86. 'jpg': 'image/jpeg',
  87. 'js': 'application/javascript',
  88. 'json': 'application/json',
  89. 'log': 'text/plain',
  90. 'm3u': 'audio/x-mpegurl',
  91. 'm4v': 'video/mp4',
  92. 'man': 'text/troff',
  93. 'mathml': 'application/mathml+xml',
  94. 'mbox': 'application/mbox',
  95. 'mdoc': 'text/troff',
  96. 'me': 'text/troff',
  97. 'mid': 'audio/midi',
  98. 'midi': 'audio/midi',
  99. 'mime': 'message/rfc822',
  100. 'mml': 'application/mathml+xml',
  101. 'mng': 'video/x-mng',
  102. 'mov': 'video/quicktime',
  103. 'mp3': 'audio/mpeg',
  104. 'mp4': 'video/mp4',
  105. 'mp4v': 'video/mp4',
  106. 'mpeg': 'video/mpeg',
  107. 'mpg': 'video/mpeg',
  108. 'ms': 'text/troff',
  109. 'msi': 'application/x-msdownload',
  110. 'odp': 'application/vnd.oasis.opendocument.presentation',
  111. 'ods': 'application/vnd.oasis.opendocument.spreadsheet',
  112. 'odt': 'application/vnd.oasis.opendocument.text',
  113. 'ogg': 'application/ogg',
  114. 'p': 'text/x-pascal',
  115. 'pas': 'text/x-pascal',
  116. 'pbm': 'image/x-portable-bitmap',
  117. 'pdf': 'application/pdf',
  118. 'pem': 'application/x-x509-ca-cert',
  119. 'pgm': 'image/x-portable-graymap',
  120. 'pgp': 'application/pgp-encrypted',
  121. 'pkg': 'application/octet-stream',
  122. 'pl': 'text/x-script.perl',
  123. 'pm': 'text/x-script.perl-module',
  124. 'png': 'image/png',
  125. 'pnm': 'image/x-portable-anymap',
  126. 'ppm': 'image/x-portable-pixmap',
  127. 'pps': 'application/vnd.ms-powerpoint',
  128. 'ppt': 'application/vnd.ms-powerpoint',
  129. 'ps': 'application/postscript',
  130. 'psd': 'image/vnd.adobe.photoshop',
  131. 'py': 'text/x-script.python',
  132. 'qt': 'video/quicktime',
  133. 'ra': 'audio/x-pn-realaudio',
  134. 'rake': 'text/x-script.ruby',
  135. 'ram': 'audio/x-pn-realaudio',
  136. 'rar': 'application/x-rar-compressed',
  137. 'rb': 'text/x-script.ruby',
  138. 'rdf': 'application/rdf+xml',
  139. 'roff': 'text/troff',
  140. 'rpm': 'application/x-redhat-package-manager',
  141. 'rss': 'application/rss+xml',
  142. 'rtf': 'application/rtf',
  143. 'ru': 'text/x-script.ruby',
  144. 's': 'text/x-asm',
  145. 'sgm': 'text/sgml',
  146. 'sgml': 'text/sgml',
  147. 'sh': 'application/x-sh',
  148. 'sig': 'application/pgp-signature',
  149. 'snd': 'audio/basic',
  150. 'so': 'application/octet-stream',
  151. 'svg': 'image/svg+xml',
  152. 'svgz': 'image/svg+xml',
  153. 'swf': 'application/x-shockwave-flash',
  154. 't': 'text/troff',
  155. 'tar': 'application/x-tar',
  156. 'tbz': 'application/x-bzip-compressed-tar',
  157. 'tcl': 'application/x-tcl',
  158. 'tex': 'application/x-tex',
  159. 'texi': 'application/x-texinfo',
  160. 'texinfo': 'application/x-texinfo',
  161. 'text': 'text/plain',
  162. 'tif': 'image/tiff',
  163. 'tiff': 'image/tiff',
  164. 'torrent': 'application/x-bittorrent',
  165. 'tr': 'text/troff',
  166. 'txt': 'text/plain',
  167. 'vcf': 'text/x-vcard',
  168. 'vcs': 'text/x-vcalendar',
  169. 'vrml': 'model/vrml',
  170. 'war': 'application/java-archive',
  171. 'wav': 'audio/x-wav',
  172. 'wma': 'audio/x-ms-wma',
  173. 'wmv': 'video/x-ms-wmv',
  174. 'wmx': 'video/x-ms-wmx',
  175. 'wrl': 'model/vrml',
  176. 'wsdl': 'application/wsdl+xml',
  177. 'xbm': 'image/x-xbitmap',
  178. 'xhtml': 'application/xhtml+xml',
  179. 'xls': 'application/vnd.ms-excel',
  180. 'xml': 'application/xml',
  181. 'xpm': 'image/x-xpixmap',
  182. 'xsl': 'application/xml',
  183. 'xslt': 'application/xslt+xml',
  184. 'yaml': 'text/yaml',
  185. 'yml': 'text/yaml',
  186. 'zip': 'application/zip',
  187. 'woff': 'application/font-woff',
  188. 'woff2': 'application/font-woff',
  189. 'otf': 'application/font-sfnt',
  190. 'otc': 'application/font-sfnt',
  191. 'ttf': 'application/font-sfnt'
  192. };
  193. var options = {
  194. port: process.env.PM2_SERVE_PORT || process.argv[3] || 8080,
  195. host: process.env.PM2_SERVE_HOST || process.argv[4] || '0.0.0.0',
  196. path: path.resolve(process.env.PM2_SERVE_PATH || process.argv[2] || '.'),
  197. spa: process.env.PM2_SERVE_SPA === 'true',
  198. homepage: process.env.PM2_SERVE_HOMEPAGE || '/index.html',
  199. basic_auth: process.env.PM2_SERVE_BASIC_AUTH === 'true' ? {
  200. username: process.env.PM2_SERVE_BASIC_AUTH_USERNAME,
  201. password: process.env.PM2_SERVE_BASIC_AUTH_PASSWORD
  202. } : null,
  203. monitor: process.env.PM2_SERVE_MONITOR
  204. };
  205. if (typeof options.port === 'string') {
  206. options.port = parseInt(options.port) || 8080
  207. }
  208. if (typeof options.monitor === 'string' && options.monitor !== '') {
  209. try {
  210. let fileContent = fs.readFileSync(path.join(process.env.PM2_HOME, 'agent.json5')).toString()
  211. // Handle old configuration with json5
  212. fileContent = fileContent.replace(/\s(\w+):/g, '"$1":')
  213. // parse
  214. let conf = JSON.parse(fileContent)
  215. options.monitorBucket = conf.public_key
  216. } catch (e) {
  217. console.log('Interaction file does not exist')
  218. }
  219. }
  220. // start an HTTP server
  221. http.createServer(function (request, response) {
  222. if (options.basic_auth) {
  223. if (!request.headers.authorization || request.headers.authorization.indexOf('Basic ') === -1) {
  224. return sendBasicAuthResponse(response)
  225. }
  226. var user = parseBasicAuth(request.headers.authorization)
  227. if (user.username !== options.basic_auth.username || user.password !== options.basic_auth.password) {
  228. return sendBasicAuthResponse(response)
  229. }
  230. }
  231. serveFile(request.url, request, response);
  232. }).listen(options.port, options.host, function (err) {
  233. if (err) {
  234. console.error(err);
  235. process.exit(1);
  236. }
  237. console.log('Exposing %s directory on %s:%d', options.path, options.host, options.port);
  238. });
  239. function serveFile(uri, request, response) {
  240. var file = decodeURIComponent(url.parse(uri || request.url).pathname);
  241. if (file === '/' || file === '') {
  242. file = options.homepage;
  243. request.wantHomepage = true;
  244. }
  245. var filePath = path.resolve(options.path + file);
  246. // since we call filesystem directly so we need to verify that the
  247. // url doesn't go outside the serve path
  248. if (filePath.indexOf(options.path) !== 0) {
  249. response.writeHead(403, { 'Content-Type': 'text/html' });
  250. return response.end('403 Forbidden');
  251. }
  252. var contentType = contentTypes[filePath.split('.').pop().toLowerCase()] || 'text/plain';
  253. fs.readFile(filePath, function (error, content) {
  254. if (error) {
  255. if ((!options.spa || file === options.homepage)) {
  256. console.error('[%s] Error while serving %s with content-type %s : %s',
  257. new Date(), filePath, contentType, error.message || error);
  258. }
  259. errorMeter.mark();
  260. if (error.code === 'ENOENT') {
  261. if (options.spa && !request.wantHomepage) {
  262. request.wantHomepage = true;
  263. return serveFile(`/${path.basename(file)}`, request, response);
  264. } else if (options.spa && file !== options.homepage) {
  265. return serveFile(options.homepage, request, response);
  266. }
  267. fs.readFile(options.path + '/404.html', function (err, content) {
  268. content = err ? '404 Not Found' : content;
  269. response.writeHead(404, { 'Content-Type': 'text/html' });
  270. return response.end(content, 'utf-8');
  271. });
  272. return;
  273. }
  274. response.writeHead(500);
  275. return response.end('Sorry, check with the site admin for error: ' + error.code + ' ..\n');
  276. }
  277. // Add CORS headers to allow browsers to fetch data directly
  278. response.writeHead(200, {
  279. 'Content-Type': contentType,
  280. 'Access-Control-Allow-Origin': '*',
  281. 'Access-Control-Allow-Methods': 'GET'
  282. });
  283. if (options.monitorBucket && contentType === 'text/html') {
  284. content = content.toString().replace('</body>', `
  285. <script>
  286. ;(function (b,e,n,o,i,t) {
  287. b[o]=b[o]||function(f){(b[o].c=b[o].c||[]).push(f)};
  288. t=e.createElement(i);e=e.getElementsByTagName(i)[0];
  289. t.async=1;t.src=n;e.parentNode.insertBefore(t,e);
  290. }(window,document,'https://apm.pm2.io/pm2-io-apm-browser.v1.js','pm2Ready','script'))
  291. pm2Ready(function(apm) {
  292. apm.setBucket('${options.monitorBucket}')
  293. apm.setApplication('${options.monitor}')
  294. apm.reportTimings()
  295. apm.reportIssues()
  296. })
  297. </script>
  298. </body>
  299. `);
  300. }
  301. response.end(content, 'utf-8');
  302. debug('[%s] Serving %s with content-type %s', Date.now(), filePath, contentType);
  303. });
  304. }
  305. function parseBasicAuth(auth) {
  306. // auth is like `Basic Y2hhcmxlczoxMjM0NQ==`
  307. var tmp = auth.split(' ');
  308. var buf = Buffer.from(tmp[1], 'base64');
  309. var plain = buf.toString();
  310. var creds = plain.split(':');
  311. return {
  312. username: creds[0],
  313. password: creds[1]
  314. }
  315. }
  316. function sendBasicAuthResponse(response) {
  317. response.writeHead(401, {
  318. 'Content-Type': 'text/html',
  319. 'WWW-Authenticate': 'Basic realm="Authentication service"'
  320. });
  321. return response.end('401 Unauthorized');
  322. }