diff --git a/README.md b/README.md index aeb653561..bb1190820 100644 --- a/README.md +++ b/README.md @@ -118,26 +118,29 @@ Run `cnc` to start the server, and visit `http://yourhostname:8000/` to view the pi@rpi3$ cnc -h Usage: cnc [options] - + + Options: - -h, --help output usage information -V, --version output the version number - -p, --port set listen port (default: 8000) - -l, --host set listen address or hostname (default: 0.0.0.0) - -b, --backlog set listen backlog (default: 511) - -c, --config set config file (default: ~/.cncrc) - -v, --verbose increase the verbosity level - -m, --mount [:] set the mount point for serving static files (default: /static:static) - -w, --watch-directory watch a directory for changes - --access-token-lifetime access token lifetime in seconds or a time span string (default: 30d) - --allow-remote-access allow remote access to the server - --controller specify CNC controller: Grbl|Smoothie|TinyG|g2core (default: '') + -p, --port Set listen port (default: 8000) + -H, --host Set listen address or hostname (default: 0.0.0.0) + -b, --backlog Set listen backlog (default: 511) + -c, --config Set config file (default: ~/.cncrc) + -v, --verbose Increase the verbosity level (-v, -vv, -vvv) + -m, --mount : Add a mount point for serving static files + -w, --watch-directory Watch a directory for changes + --access-token-lifetime Access token lifetime in seconds or a time span string (default: 30d) + --allow-remote-access Allow remote access to the server (default: false) + --controller Specify CNC controller: Grbl|Smoothie|TinyG|g2core (default: '') + -h, --help output usage information Examples: $ cnc -vv $ cnc --mount /pendant:/home/pi/tinyweb + $ cnc --mount /widget:~+/widget --mount /pendant:~/pendant + $ cnc --mount /widget:https://cncjs.github.io/cncjs-widget-boilerplate/ $ cnc --watch-directory /home/pi/watch $ cnc --access-token-lifetime 60d # e.g. 3600, 30m, 12h, 30d $ cnc --allow-remote-access diff --git a/package.json b/package.json index 4446ee780..85a6f5e3b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "start": "./bin/cnc", "start-electron": "electron ./dist/cnc/main", "watch-dev": "webpack --watch --config webpack.appconfig.development.js", - "start-dev": "NODE_ENV=development ./bin/cnc -vv -p 8000 -m /static:~+/static -m /widget:~+/widget", + "start-dev": "NODE_ENV=development ./bin/cnc -vv -p 8000 -m /static:~+/static", + "start-dev-widget": "NODE_ENV=development ./bin/cnc -vv -p 8000 -m /static:~+/static -m /widget:https://cncjs.github.io/cncjs-widget-boilerplate/", "dev": "npm run build-dev && npm run start-dev", "prod": "npm run build-prod && NODE_ENV=production ./bin/cnc", "lint": "npm run eslint && npm run stylint", diff --git a/src/app/index.js b/src/app/index.js index b3cd86f41..b8831da7e 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -3,18 +3,25 @@ import dns from 'dns'; import fs from 'fs'; import os from 'os'; import path from 'path'; -import set from 'lodash/set'; -import size from 'lodash/size'; +import url from 'url'; import bcrypt from 'bcrypt-nodejs'; import chalk from 'chalk'; +import expandTilde from 'expand-tilde'; +import express from 'express'; +import httpProxy from 'http-proxy'; +import escapeRegExp from 'lodash/escapeRegExp'; +import set from 'lodash/set'; +import size from 'lodash/size'; +import trimEnd from 'lodash/trimEnd'; import webappengine from 'webappengine'; +import settings from './config/settings'; import app from './app'; import cncengine from './services/cncengine'; import monitor from './services/monitor'; import config from './services/configstore'; import ensureArray from './lib/ensure-array'; import logger from './lib/logger'; -import settings from './config/settings'; +import urljoin from './lib/urljoin'; const log = logger('init'); @@ -97,36 +104,97 @@ const createServer = (options, callback) => { const routes = []; ensureArray(options.mountPoints).forEach(mount => { - const { route = '', directory = '' } = mount; - const cjs = { - route: chalk.yellow(JSON.stringify(route)), - directory: chalk.yellow(JSON.stringify(directory)) - }; - - log.info(`Mounting a directory ${cjs.directory} for the route ${cjs.route}`); - - if (!route) { - log.error(`Must specify a valid route path ${cjs.route}.`); - return; - } - if (!directory) { - log.error(`The directory path ${cjs.directory} must not be empty.`); - return; - } - if (!path.isAbsolute(directory)) { - log.error(`The directory path ${cjs.directory} must be absolute.`); - return; - } - if (!fs.existsSync(directory)) { - log.error(`The directory path ${cjs.directory} does not exist.`); + if (!mount || !mount.route || mount.route === '/') { + log.error(`Must specify a valid route path ${JSON.stringify(mount.route)}.`); return; } - routes.push({ - type: 'static', - route: route, - directory: directory - }); + if (mount.target.match(/^(http|https):\/\//i)) { + log.info(`Starting a proxy server to proxy all requests starting with ${chalk.yellow(mount.route)} to ${chalk.yellow(mount.target)}`); + + routes.push({ + type: 'server', + route: mount.route, + server: (options) => { + // route + // > '/custom-widget/' + // routeWithoutTrailingSlash + // > '/custom-widget' + // target + // > 'https://cncjs.github.io/cncjs-widget-boilerplate/' + // targetPathname + // > '/cncjs-widget-boilerplate/' + // proxyPathPattern + // > RegExp('^/cncjs-widget-boilerplate/custom-widget') + const { route = '/' } = { ...options }; + const routeWithoutTrailingSlash = trimEnd(route, '/'); + const target = mount.target; + const targetPathname = url.parse(target).pathname; + const proxyPathPattern = new RegExp('^' + escapeRegExp(urljoin(targetPathname, routeWithoutTrailingSlash)), 'i'); + + log.debug(`> route=${chalk.yellow(route)}`); + log.debug(`> routeWithoutTrailingSlash=${chalk.yellow(routeWithoutTrailingSlash)}`); + log.debug(`> target=${chalk.yellow(target)}`); + log.debug(`> targetPathname=${chalk.yellow(targetPathname)}`); + log.debug(`> proxyPathPattern=RegExp(${chalk.yellow(proxyPathPattern)})`); + + const proxy = httpProxy.createProxyServer({ + // Change the origin of the host header to the target URL + changeOrigin: true, + + // Do not verify the SSL certificate for self-signed certs + //secure: false, + + target: target + }); + + proxy.on('proxyReq', (proxyReq, req, res, options) => { + const originalPath = proxyReq.path || ''; + proxyReq.path = originalPath + .replace(proxyPathPattern, targetPathname) + .replace('//', '/'); + + log.debug(`proxy.on('proxyReq'): modifiedPath=${chalk.yellow(proxyReq.path)}, originalPath=${chalk.yellow(originalPath)}`); + }); + + proxy.on('proxyRes', (proxyRes, req, res) => { + log.debug(`proxy.on('proxyRes'): headers=${JSON.stringify(proxyRes.headers, true, 2)}`); + }); + + const app = express(); + app.all(urljoin(routeWithoutTrailingSlash, '*'), (req, res) => { + log.debug(`proxy.web(): req.url=${chalk.yellow(req.url)}`); + proxy.web(req, res); + }); + + return app; + } + }); + } else { + // expandTilde('~') => '/Users/' + const directory = expandTilde(mount.target || '').trim(); + + log.info(`Mounting a directory ${chalk.yellow(JSON.stringify(directory))} to serve requests starting with ${chalk.yellow(mount.route)}`); + + if (!directory) { + log.error(`The directory path ${chalk.yellow(JSON.stringify(directory))} must not be empty.`); + return; + } + if (!path.isAbsolute(directory)) { + log.error(`The directory path ${chalk.yellow(JSON.stringify(directory))} must be absolute.`); + return; + } + if (!fs.existsSync(directory)) { + log.error(`The directory path ${chalk.yellow(JSON.stringify(directory))} does not exist.`); + return; + } + + routes.push({ + type: 'static', + route: mount.route, + directory: directory + }); + } }); routes.push({ @@ -160,7 +228,7 @@ const createServer = (options, callback) => { }); if (address !== '0.0.0.0') { - log.info('Starting the server at ' + chalk.cyan(`http://${address}:${port}`)); + log.info('Starting the server at ' + chalk.yellow(`http://${address}:${port}`)); return; } @@ -171,7 +239,7 @@ const createServer = (options, callback) => { } addresses.forEach(({ address, family }) => { - log.info('Starting the server at ' + chalk.cyan(`http://${address}:${port}`)); + log.info('Starting the server at ' + chalk.yellow(`http://${address}:${port}`)); }); }); }) diff --git a/src/cnc.js b/src/cnc.js index 9f5b10e88..f9a934100 100644 --- a/src/cnc.js +++ b/src/cnc.js @@ -2,7 +2,6 @@ /* eslint no-console: 0 */ import path from 'path'; import program from 'commander'; -import expandTilde from 'expand-tilde'; import pkg from './package.json'; // Defaults to 'production' @@ -17,17 +16,17 @@ const parseMountPoint = (val, acc) => { const mount = { route: '/', - directory: val + target: val }; if (val.indexOf(':') >= 0) { const r = val.match(/(?:([^:]*)(?::(.*)))/); mount.route = r[1]; - mount.directory = r[2]; + mount.target = r[2]; } - mount.route = path.join('/', mount.route || ''); // path.join('/', 'pendant') => '/pendant' - mount.directory = expandTilde(mount.directory || ''); // expandTilde('~') => '/Users/' + mount.route = path.join('/', mount.route || '').trim(); // path.join('/', 'pendant') => '/pendant' + mount.target = (mount.target || '').trim(); acc.push(mount); @@ -52,7 +51,7 @@ program .option('-b, --backlog ', 'Set listen backlog (default: 511)', 511) .option('-c, --config ', 'Set config file (default: ~/.cncrc)') .option('-v, --verbose', 'Increase the verbosity level (-v, -vv, -vvv)', increaseVerbosityLevel, 0) - .option('-m, --mount :', 'Add a mount point for serving static files', parseMountPoint, []) + .option('-m, --mount :', 'Add a mount point for serving static files', parseMountPoint, []) .option('-w, --watch-directory ', 'Watch a directory for changes') .option('--access-token-lifetime ', 'Access token lifetime in seconds or a time span string (default: 30d)') .option('--allow-remote-access', 'Allow remote access to the server (default: false)') @@ -64,7 +63,8 @@ program.on('--help', () => { console.log(''); console.log(' $ cnc -vv'); console.log(' $ cnc --mount /pendant:/home/pi/tinyweb'); - console.log(' $ cnc --mount /widgets:~/widgets --mount /pendant:~/pendant'); + console.log(' $ cnc --mount /widget:~+/widget --mount /pendant:~/pendant'); + console.log(' $ cnc --mount /widget:https://cncjs.github.io/cncjs-widget-boilerplate/'); console.log(' $ cnc --watch-directory /home/pi/watch'); console.log(' $ cnc --access-token-lifetime 60d # e.g. 3600, 30m, 12h, 30d'); console.log(' $ cnc --allow-remote-access'); diff --git a/src/web/widgets/Custom/Custom.jsx b/src/web/widgets/Custom/Custom.jsx index 2a53fa5fe..c6eeb32b9 100644 --- a/src/web/widgets/Custom/Custom.jsx +++ b/src/web/widgets/Custom/Custom.jsx @@ -4,6 +4,7 @@ import pubsub from 'pubsub-js'; import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; import ReactDOM from 'react-dom'; +import settings from '../../config/settings'; import store from '../../store'; import Iframe from '../../components/Iframe'; import ResizeObserver from '../../lib/ResizeObserver'; @@ -69,6 +70,7 @@ class Custom extends PureComponent { const target = get(this.iframe, 'contentWindow'); const message = { token: token, + version: settings.version, action: { type: type, payload: { diff --git a/src/web/widgets/Custom/Settings.jsx b/src/web/widgets/Custom/Settings.jsx index 0d5ae98f4..71e6e1f35 100644 --- a/src/web/widgets/Custom/Settings.jsx +++ b/src/web/widgets/Custom/Settings.jsx @@ -73,7 +73,7 @@ class Settings extends PureComponent { }} type="url" className="form-control" - placeholder="/widget" + placeholder="/widget/" defaultValue={url} />