Skip to content

Commit

Permalink
Support for proxying requests starting with a mount path to an extern…
Browse files Browse the repository at this point in the history
…al url
  • Loading branch information
cheton committed Oct 31, 2017
1 parent 2bf0e2c commit a1cbada
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 53 deletions.
27 changes: 15 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <filename> set config file (default: ~/.cncrc)
-v, --verbose increase the verbosity level
-m, --mount [<url>:]<path> set the mount point for serving static files (default: /static:static)
-w, --watch-directory <path> watch a directory for changes
--access-token-lifetime <lifetime> access token lifetime in seconds or a time span string (default: 30d)
--allow-remote-access allow remote access to the server
--controller <type> specify CNC controller: Grbl|Smoothie|TinyG|g2core (default: '')
-p, --port <port> Set listen port (default: 8000)
-H, --host <host> Set listen address or hostname (default: 0.0.0.0)
-b, --backlog <backlog> Set listen backlog (default: 511)
-c, --config <filename> Set config file (default: ~/.cncrc)
-v, --verbose Increase the verbosity level (-v, -vv, -vvv)
-m, --mount <route-path>:<target> Add a mount point for serving static files
-w, --watch-directory <path> Watch a directory for changes
--access-token-lifetime <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 <type> 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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
132 changes: 100 additions & 32 deletions src/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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/<userhome>'
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({
Expand Down Expand Up @@ -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;
}

Expand All @@ -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}`));
});
});
})
Expand Down
14 changes: 7 additions & 7 deletions src/cnc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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/<userhome>'
mount.route = path.join('/', mount.route || '').trim(); // path.join('/', 'pendant') => '/pendant'
mount.target = (mount.target || '').trim();

acc.push(mount);

Expand All @@ -52,7 +51,7 @@ program
.option('-b, --backlog <backlog>', 'Set listen backlog (default: 511)', 511)
.option('-c, --config <filename>', 'Set config file (default: ~/.cncrc)')
.option('-v, --verbose', 'Increase the verbosity level (-v, -vv, -vvv)', increaseVerbosityLevel, 0)
.option('-m, --mount <route-path>:<directory-path>', 'Add a mount point for serving static files', parseMountPoint, [])
.option('-m, --mount <route-path>:<target>', 'Add a mount point for serving static files', parseMountPoint, [])
.option('-w, --watch-directory <path>', 'Watch a directory for changes')
.option('--access-token-lifetime <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)')
Expand All @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions src/web/widgets/Custom/Custom.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/web/widgets/Custom/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class Settings extends PureComponent {
}}
type="url"
className="form-control"
placeholder="/widget"
placeholder="/widget/"
defaultValue={url}
/>
</div>
Expand Down

0 comments on commit a1cbada

Please sign in to comment.