diff --git a/bin/pm2 b/bin/pm2 index fc1c216c..e968a249 100755 --- a/bin/pm2 +++ b/bin/pm2 @@ -961,9 +961,13 @@ commander.command('deepUpdate') commander.command('serve [path] [port]') .alias('expose') .option('--port [port]', 'specify port to listen to') + .option('--spa', 'always serving index.html on inexistant sub path') + .option('--basic-auth-username [username]', 'set basic auth username') + .option('--basic-auth-password [password]', 'set basic auth password') + .option('--monitor [frontend-app]', 'frontend app monitoring (auto integrate snippet on html files)') .description('serve a directory over http via port') .action(function (path, port, cmd) { - pm2.serve(path, port || cmd.port, commander); + pm2.serve(path, port || cmd.port, cmd, commander); }); commander.command('examples') diff --git a/lib/API.js b/lib/API.js index d928b440..4fee0227 100644 --- a/lib/API.js +++ b/lib/API.js @@ -868,6 +868,7 @@ class API { _startJson (file, opts, action, pipe, cb) { var config = {}; var appConf = {}; + var staticConf = []; var deployConf = {}; var apps_info = []; var that = this; @@ -911,6 +912,8 @@ class API { */ if (config.deploy) deployConf = config.deploy; + if (config.static) + staticConf = config.static; if (config.apps) appConf = config.apps; else if (config.pm2) @@ -929,6 +932,24 @@ class API { var apps_name = []; var proc_list = {}; + // Add statics to apps + staticConf.forEach(function(serve) { + appConf.push({ + name: serve.name ? serve.name : `static-page-server-${serve.port}`, + script: path.resolve(__dirname, 'API', 'Serve.js'), + env: { + PM2_SERVE_PORT: serve.port, + PM2_SERVE_PATH: serve.path, + PM2_SERVE_SPA: serve.spa, + PM2_SERVE_DIRECTORY: serve.directory, + PM2_SERVE_BASIC_AUTH: serve.basic_auth !== undefined, + PM2_SERVE_BASIC_AUTH_USERNAME: serve.basic_auth ? serve.basic_auth.username : null, + PM2_SERVE_BASIC_AUTH_PASSWORD: serve.basic_auth ? serve.basic_auth.password : null, + PM2_SERVE_MONITOR: serve.monitor + } + }); + }); + // Here we pick only the field we want from the CLI when starting a JSON appConf.forEach(function(app) { if (!app.env) { app.env = {}; } diff --git a/lib/API/Extra.js b/lib/API/Extra.js index 22d5efa6..a07d8312 100644 --- a/lib/API/Extra.js +++ b/lib/API/Extra.js @@ -467,21 +467,36 @@ module.exports = function(CLI) { * @param {Object} opts options * @param {String} opts.path path to be served * @param {Number} opts.port port on which http will bind + * @param {Boolean} opts.spa single page app served + * @param {String} opts.basicAuthUsername basic auth username + * @param {String} opts.basicAuthPassword basic auth password + * @param {Object} commander commander object * @param {Function} cb optional callback */ - CLI.prototype.serve = function (target_path, port, opts, cb) { + CLI.prototype.serve = function (target_path, port, opts, commander, cb) { var that = this; var servePort = process.env.PM2_SERVE_PORT || port || 8080; var servePath = path.resolve(process.env.PM2_SERVE_PATH || target_path || '.'); var filepath = path.resolve(path.dirname(module.filename), './Serve.js'); - if (!opts.name || typeof(opts.name) == 'function') - opts.name = 'static-page-server-' + servePort; + if (typeof commander.name === 'string') + opts.name = commander.name + else + opts.name = 'static-page-server-' + servePort if (!opts.env) opts.env = {}; opts.env.PM2_SERVE_PORT = servePort; opts.env.PM2_SERVE_PATH = servePath; + opts.env.PM2_SERVE_SPA = opts.spa; + if (opts.basicAuthUsername && opts.basicAuthPassword) { + opts.env.PM2_SERVE_BASIC_AUTH = 'true'; + opts.env.PM2_SERVE_BASIC_AUTH_USERNAME = opts.basicAuthUsername; + opts.env.PM2_SERVE_BASIC_AUTH_PASSWORD = opts.basicAuthPassword; + } + if (opts.monitor) { + opts.env.PM2_SERVE_MONITOR = opts.monitor + } opts.cwd = servePath; this.start(filepath, opts, function (err, res) { diff --git a/lib/API/Serve.js b/lib/API/Serve.js index 20d1252b..493a6b96 100644 --- a/lib/API/Serve.js +++ b/lib/API/Serve.js @@ -3,6 +3,8 @@ * Use of this source code is governed by a license that * can be found in the LICENSE file. */ +'use strict'; + var fs = require('fs'); var http = require('http'); var url = require('url'); @@ -197,18 +199,60 @@ var contentTypes = { 'ttf': 'application/font-sfnt' }; - var options = { port: process.env.PM2_SERVE_PORT || process.argv[3] || 8080, - path: path.resolve(process.env.PM2_SERVE_PATH || process.argv[2] || '.') + path: path.resolve(process.env.PM2_SERVE_PATH || process.argv[2] || '.'), + spa: process.env.PM2_SERVE_SPA === 'true', + homepage: process.env.PM2_SERVE_HOMEPAGE || '/index.html', + basic_auth: process.env.PM2_SERVE_BASIC_AUTH === 'true' ? { + username: process.env.PM2_SERVE_BASIC_AUTH_USERNAME, + password: process.env.PM2_SERVE_BASIC_AUTH_PASSWORD + } : null, + monitor: process.env.PM2_SERVE_MONITOR }; +if (typeof options.monitor === 'string' && options.monitor !== '') { + try { + let fileContent = fs.readFileSync(path.join(process.env.PM2_HOME, 'agent.json5')).toString() + // Handle old configuration with json5 + fileContent = fileContent.replace(/\s(\w+):/g, '"$1":') + // parse + let conf = JSON.parse(fileContent) + options.monitorBucket = conf.public_key + } catch (e) { + console.log('Interaction file does not exist') + } +} + // start an HTTP server http.createServer(function (request, response) { - var file = url.parse(request.url).pathname; + if (options.basic_auth) { + if (!request.headers.authorization || request.headers.authorization.indexOf('Basic ') === -1) { + return sendBasicAuthResponse(response) + } + + var user = parseBasicAuth(request.headers.authorization) + if (user.username !== options.basic_auth.username || user.password !== options.basic_auth.password) { + return sendBasicAuthResponse(response) + } + } + + serveFile(request.url, request, response); + +}).listen(options.port, function (err) { + if (err) { + console.error(err); + process.exit(1); + } + console.log('Exposing %s directory on port %d', options.path, options.port); +}); + +function serveFile(uri, request, response) { + var file = url.parse(uri || request.url).pathname; if (file === '/' || file === '') { - file = '/index.html'; + file = options.homepage; + request.wantHomepage = true; } var filePath = path.resolve(options.path + file); @@ -223,30 +267,72 @@ http.createServer(function (request, response) { fs.readFile(filePath, function (error, content) { if (error) { - console.error('[%s] Error while serving %s with content-type %s : %s', - Date.now(), filePath, contentType, error.message || error); + if ((!options.spa || request.wantHomepage)) { + console.error('[%s] Error while serving %s with content-type %s : %s', + new Date(), filePath, contentType, error.message || error); + } if (!isNode4) errorMeter.mark(); if (error.code === 'ENOENT') { + if (options.spa && !request.wantHomepage) { + request.wantHomepage = true; + return serveFile(`/${path.basename(file)}`, request, response); + } else if (options.spa && file !== options.homepage) { + return serveFile(options.homepage, request, response); + } fs.readFile(options.path + '/404.html', function (err, content) { content = err ? '404 Not Found' : content; response.writeHead(404, { 'Content-Type': 'text/html' }); - response.end(content, 'utf-8'); + return response.end(content, 'utf-8'); }); - } else { - response.writeHead(500); - response.end('Sorry, check with the site admin for error: ' + error.code + ' ..\n'); + return; } - } else { - response.writeHead(200, { 'Content-Type': contentType }); - response.end(content, 'utf-8'); - debug('[%s] Serving %s with content-type %s', Date.now(), filePath, contentType); + response.writeHead(500); + return response.end('Sorry, check with the site admin for error: ' + error.code + ' ..\n'); } + response.writeHead(200, { 'Content-Type': contentType }); + if (options.monitorBucket && contentType === 'text/html') { + content = content.toString().replace('', ` + + +`); + } + response.end(content, 'utf-8'); + debug('[%s] Serving %s with content-type %s', Date.now(), filePath, contentType); }); -}).listen(options.port, function (err) { - if (err) { - console.error(err); - process.exit(1); +} + +function parseBasicAuth(auth) { + // auth is like `Basic Y2hhcmxlczoxMjM0NQ==` + var tmp = auth.split(' '); + + var buf = Buffer.from(tmp[1], 'base64'); + var plain = buf.toString(); + + var creds = plain.split(':'); + return { + username: creds[0], + password: creds[1] } - console.log('Exposing %s directory on port %d', options.path, options.port); -}); +} + +function sendBasicAuthResponse(response) { + response.writeHead(401, { + 'Content-Type': 'text/html', + 'WWW-Authenticate': 'Basic realm="Authentication service"' + }); + return response.end('401 Unauthorized'); +} \ No newline at end of file diff --git a/test/e2e/cli/serve.sh b/test/e2e/cli/serve.sh index 8defad52..8f05257a 100644 --- a/test/e2e/cli/serve.sh +++ b/test/e2e/cli/serve.sh @@ -29,6 +29,68 @@ OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` [ $OUT -eq 0 ] || fail "should be offline" success "should be offline" +echo "testing SPA" +$pm2 serve . $PORT --spa +should 'should have started serving dir' 'online' 1 + +curl http://localhost:$PORT/ > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` +[ $OUT -eq 1 ] || fail "should have served index file under /index.html" +success "should have served index file under /index.html" + +curl http://localhost:$PORT/index.html > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` +[ $OUT -eq 1 ] || fail "should have served index file under /index.html" +success "should have served index file under /index.html" + +curl http://localhost:$PORT/other.html > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | wc -l` +[ $OUT -eq 2 ] || fail "should have served file under /other.html" +success "should have served file under /other.html" + +curl http://localhost:$PORT/mangezdespommes/avecpepin/lebref > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` +[ $OUT -eq 1 ] || fail "should have served index file under /index.html" +success "should have served index file under /index.html" + +curl http://localhost:$PORT/mangezdespommes/avecpepin/lebref/other.html > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | wc -l` +[ $OUT -eq 2 ] || fail "should have served file under /other.html" +success "should have served file under /other.html" + +echo "Shutting down the server" +$pm2 delete all + +echo "testing basic auth" +$pm2 serve . $PORT --basic-auth-username user --basic-auth-password pass +should 'should have started serving dir' 'online' 1 + +curl http://user:pass@localhost:$PORT/index.html > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` +[ $OUT -eq 1 ] || fail "should have served index file under /index.html" +success "should have served index file under /index.html" + +echo "Shutting down the server" +$pm2 delete all + +echo "Testing with static ecosystem" + +$pm2 start ecosystem-serve.json +should 'should have started serving dir' 'online' 1 + +curl http://user:pass@localhost:8081/index.html > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` +[ $OUT -eq 1 ] || fail "should be listening on port 8081" +success "should be listening on port 8081" + +curl http://user:pass@localhost:8081/mangezdesmangues/aupakistan > /tmp/tmp_out.txt +OUT=`cat /tmp/tmp_out.txt | grep -o "good shit" | wc -l` +[ $OUT -eq 1 ] || fail "should be listening on port 8081" +success "should be listening on port 8081" + +echo "Shutting down the server" +$pm2 delete all + $pm2 serve . $PORT_2 should 'should have started serving dir' 'online' 1 diff --git a/test/fixtures/serve/ecosystem-serve.json b/test/fixtures/serve/ecosystem-serve.json new file mode 100644 index 00000000..32a431ab --- /dev/null +++ b/test/fixtures/serve/ecosystem-serve.json @@ -0,0 +1,13 @@ +{ + "static": [ + { + "path": ".", + "spa": true, + "basic_auth": { + "username": "user", + "password": "pass" + }, + "port": 8081 + } + ] +} \ No newline at end of file diff --git a/test/fixtures/serve/other.html b/test/fixtures/serve/other.html new file mode 100644 index 00000000..bee787ac --- /dev/null +++ b/test/fixtures/serve/other.html @@ -0,0 +1,2 @@ +iam another file +with 2 lines