diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..69109601 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +**/node_modules +**/package-lock.json +react-ui/nojsx +wsc-chrome.min.js +assets +package.json +package.zip diff --git a/README.md b/README.md index c08b92b9..d99bf345 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ handlers is an array of 2 element arrays where the first item is a regular expre ``` cd web-server-chrome +mkdir assets cd makedeps npm install npm run make # this builds the app dependencies such as react and material-ui into a bundle diff --git a/background.js b/background.js index a464d6d5..30b1bf3f 100644 --- a/background.js +++ b/background.js @@ -33,7 +33,11 @@ function onchoosefolder(entry) { function settings_ready(d) { localOptions = d - console.log('settings:',d) + let dCpy = {}; + Object.assign(dCpy, d); + delete dCpy.optPrivateKey;// dont fill logs with crypto info + delete dCpy.optCertificate; + console.log('settings:',dCpy) setTimeout( maybeStartup, 2000 ) // give background accept handler some time to trigger //chrome.alarms.getAll( onAllAlarms ) } diff --git a/chromise.js b/chromise.js new file mode 100644 index 00000000..4b7b1694 --- /dev/null +++ b/chromise.js @@ -0,0 +1,183 @@ +/** + * @author Alexey Kuzmin + * @fileoverview Promise based wrapper for Chrome Extension API. + * @see https://developer.chrome.com/extensions/api_index + * @license MIT + * @version 3.1.0 + */ + + + +;(function(global) { + 'use strict'; + + let apiProxy = { + /** + * @param {!Object} apiObject + * @param {string} methodName + * @param {Arguments} callArguments Arguments to be passes to method call. + */ + callMethod(apiObject, methodName, callArguments) { + let originalMethod = apiObject[methodName]; + let callArgumentsArray = Array.from(callArguments); + + return new Promise((resolve, reject) => { + let callback = apiProxy.processResponse_.bind(null, resolve, reject); + callArgumentsArray.push(callback); + originalMethod.apply(apiObject, callArgumentsArray); + }); + }, + + /** + * @param {!Function} callback + * @param {!Function} errback + * @param {!Array} response Response from Extension API. + * @private + */ + processResponse_(callback, errback, ...response) { + let error = global.chrome.runtime.lastError; + if (typeof error == 'object') { + errback(new Error(error.message)); + return; + } + + if (response.length < 2) + response = response[0]; // undefined if response is empty + + callback(response); + } + }; + + + let classifier = { + /** + * @param {string} letter + * @return {boolean} + * @private + */ + isCapitalLetter_(letter) { + return letter == letter.toUpperCase(); + }, + + /** + * @param {string} string + * @return {boolean} + * @private + */ + startsWithCapitalLetter_(string) { + return classifier.isCapitalLetter_(string[0]); + }, + + /** + * We need to decide should given property be wrapped or not + * by its name only. Retrieving its value would cause API initialization, + * that can take a long time (dozens of ms). + * @param {string} propName + * @return {boolean} + */ + propertyNeedsWrapping(propName) { + if (classifier.startsWithCapitalLetter_(propName)) { + // Either constructor, enum, or constant. + return false; + } + + if (propName.startsWith('on') && + classifier.isCapitalLetter_(propName[2])) { + // Extension API event, e.g. 'onUpdated'. + return false; + } + + // Must be a namespace or a method. + return true; + } + }; + + + let wrapGuy = { + /** + * @param {!Object} api API object to wrap. + * @return {!Object} + */ + wrapApi(api) { + return wrapGuy.wrapObject_(api); + }, + + /** + * Wraps API object. + * @param {!Object} apiObject + * @return {!Object} + * @private + */ + wrapObject_(apiObject) { + let wrappedObject = {}; + + Object.keys(apiObject) + .filter(classifier.propertyNeedsWrapping) + .forEach(keyName => { + Object.defineProperty(wrappedObject, keyName, { + enumerable: true, + configurable: true, + get() { + return wrapGuy.wrapObjectField_(apiObject, keyName); + } + }); + }); + + return wrappedObject; + }, + + /** + * @type {!Map} + * @private + */ + wrappedFieldsCache_: new Map(), + + /** + * Wraps single object field. + * @param {!Object} apiObject + * @param {string} keyName + * @return {?|undefined} + * @private + */ + wrapObjectField_(apiObject, keyName) { + let apiEntry = apiObject[keyName]; + + if (wrapGuy.wrappedFieldsCache_.has(apiEntry)) { + return wrapGuy.wrappedFieldsCache_.get(apiEntry); + } + + let entryType = typeof apiEntry; + let wrappedField; + if (entryType == 'function') { + wrappedField = wrapGuy.wrapMethod_(apiObject, keyName); + } + if (entryType == 'object') { + wrappedField = wrapGuy.wrapObject_(apiEntry); + } + + if (wrappedField) { + wrapGuy.wrappedFieldsCache_.set(apiEntry, wrappedField); + return wrappedField; + } + }, + + /** + * Wraps API method. + * @param {!Object} apiObject + * @param {string} methodName + * @return {!Function} + * @private + */ + wrapMethod_(apiObject, methodName) { + return function() { + return apiProxy.callMethod(apiObject, methodName, arguments); + } + } + }; + + + let chromise = wrapGuy.wrapApi(global.chrome); + + global.chromise = chromise; + +}(this)); diff --git a/common.js b/common.js index a65a4a53..3af74507 100644 --- a/common.js +++ b/common.js @@ -205,11 +205,12 @@ function ui82arr(arr, startOffset) { return outarr } function str2ab(s) { - var arr = [] + var buf = new ArrayBuffer(s.length); + var bufView = new Uint8Array(buf); for (var i=0; i { return createCrypto(name, data || {}); } + +})(); + diff --git a/makedeps/README.md b/makedeps/README.md new file mode 100644 index 00000000..f62a5826 --- /dev/null +++ b/makedeps/README.md @@ -0,0 +1 @@ +run `yarn make` or `npm run make` to build the dependency bundle for the main app \ No newline at end of file diff --git a/makedeps/index.js b/makedeps/index.js new file mode 100644 index 00000000..b922d82e --- /dev/null +++ b/makedeps/index.js @@ -0,0 +1,21 @@ +var m + +m = require('react') +window.React = m + +m = require('react-dom') +window.ReactDOM = m + +m = require('@material-ui/core') +window.MaterialUI = m + +m = require('@material-ui/lab'); +window.MaterialUILab = m + +m = require('underscore') +window._ = m + +m = require('node-forge') +window.forge = m + +module.exports = {} diff --git a/makedeps/package.json b/makedeps/package.json new file mode 100644 index 00000000..d4efff4f --- /dev/null +++ b/makedeps/package.json @@ -0,0 +1,19 @@ +{ + "name": "makedeps", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "make": "./node_modules/.bin/browserify index.js > ../assets/bundle.js" + }, + "dependencies": { + "@material-ui/core": "^4.6.1", + "@material-ui/icons": "^4.5.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "browserify": "^16.5.0", + "node-forge": "^0.10.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "underscore": "^1.9.1" + } +} diff --git a/manifest.json b/manifest.json index a55664f4..81854e39 100644 --- a/manifest.json +++ b/manifest.json @@ -10,7 +10,9 @@ "minimum_chrome_version": "45", "app": { "background": { - "scripts": ["underscore.js","encoding.js","common.js","log-full.js","mime.js","buffer.js","request.js","stream.js","chromesocketxhr.js","connection.js","webapp.js","websocket.js","handlers.js","httplib.js","upnp.js","background.js"] + "scripts": ["underscore.js","encoding.js","common.js","assets/bundle.js", + "log-full.js", "mime.js", "buffer.js","request.js","crypto.js","stream.js", "chromesocketxhr.js", + "connection.js","webapp.js","websocket.js","handlers.js","httplib.js","upnp.js","background.js"] } }, "permissions": [ diff --git a/minimize.sh b/minimize.sh index 25e50b17..2862f793 100644 --- a/minimize.sh +++ b/minimize.sh @@ -1 +1 @@ -cat "underscore.js" "encoding.js" "common.js" "log-full.js" "mime.js" "buffer.js" "request.js" "stream.js" "chromesocketxhr.js" "connection.js" "webapp.js" "websocket.js" "upnp.js" "handlers.js" "httplib.js" > wsc-chrome.min.js +cat "underscore.js" "encoding.js" "common.js" "log-full.js" "mime.js" "buffer.js" "request.js" "crypto.js" "stream.js" "chromesocketxhr.js" "connection.js" "webapp.js" "websocket.js" "upnp.js" "handlers.js" "httplib.js" > wsc-chrome.min.js diff --git a/react-ui/.babelrc b/react-ui/.babelrc new file mode 100644 index 00000000..59566c26 --- /dev/null +++ b/react-ui/.babelrc @@ -0,0 +1,6 @@ +{ + "plugins": [ + ["@babel/plugin-transform-react-jsx",{useBuiltIns:true}], + "@babel/plugin-syntax-class-properties" + ] +} diff --git a/react-ui/js/index.js b/react-ui/js/index.js index 7dc31237..65776158 100644 --- a/react-ui/js/index.js +++ b/react-ui/js/index.js @@ -12,13 +12,15 @@ const { Toolbar, Typography, Button, - ThemeProvider, + ThemeProvider } = MaterialUI +const {Alert} = MaterialUILab; + const {createMuiTheme, colors, withStyles} = MaterialUI; const styles = { card: {margin: '10px'}, - appicon: {marginRight: '10px'}, + appicon: {marginRight: '10px'} }; const theme = createMuiTheme({ palette: { @@ -84,7 +86,25 @@ const functions = { webapp.opts.optBackground = val bg.backgroundSettingChange({'optBackground':val}) } - } + }, + optPrivateKey: (app, k, val) => { + //console.log('privateKey') + console.assert(typeof val === 'string') + app.webapp.updateOption('optPrivateKey', val); + }, + optCertificate: (app, k, val) => { + //console.log('certificate'); + console.assert(typeof val === 'string') + app.webapp.updateOption('optCertificate', val); + }, + optUseHttps: (app, k, val) => { + console.log("useHttps", val); + app.webapp.updateOption('optUseHttps', val); + if (app.webapp.started) { + app.webapp.stop(); + app.webapp.start(); + } + } }; @@ -118,7 +138,7 @@ class App extends React.Component { starting: false, lasterr: null, folder: null, - message: '', + message: '' } constructor(props) { super(props) @@ -133,7 +153,12 @@ class App extends React.Component { } settings_ready() { const allOpts = this.appOptions.getAll() - console.log('fetched local settings', this.appOptions, allOpts) + let dCpy = {}; + Object.assign(dCpy, allOpts); + delete dCpy.optPrivateKey;// dont fill logs with crypto info + delete dCpy.optCertificate; + + console.log('fetched local settings', this.appOptions, dCpy) this.webapp = this.bg.get_webapp(allOpts) // retainStr in here this.bg.WSC.VERBOSE = this.bg.WSC.DEBUG = this.appOptions.get('optVerbose') this.webapp.on_status_change = this.on_webapp_change.bind(this) @@ -174,6 +199,22 @@ class App extends React.Component { interfaces: this.webapp.urls.slice() }) } + gen_crypto() { + let reasonStr = this.webapp.opts.optPrivateKey ? "private key" : + this.webapp.opts.optCertificate ? "certificate" : ""; + if (reasonStr) { + console.warn("Would overwrite existing " + reasonStr + ", erase it first\nMake sure to save a copy first"); + return; + } + let cn = "WebServerForChrome" + (new Date()).toISOString(); + let data = this.webapp.createCrypto(cn); + this.appOptions.set('optPrivateKey', data[cn].privateKey); + this.appOptions.set('optCertificate', data[cn].cert); + this.webapp.updateOption('optPrivateKey', data[cn].privateKey); + this.webapp.updateOption('optCertificate', data[cn].cert); + this.setState({optPrivateKey: data[cn].privateKey, optCertificate: data[cn].cert}); + setTimeout(this.render, 50); // prevent race condition when ReactElement get set before opts have value + } ui_ready() { if (this.webapp) { if (! (this.webapp.started || this.webapp.starting)) { @@ -225,9 +266,14 @@ class App extends React.Component { optModRewriteEnable: null, optModRewriteRegexp: ['optModRewriteEnable'], optModRewriteNegate: ['optModRewriteEnable'], - optModRewriteTo: ['optModRewriteEnable'] - } - console.assert(this) + optModRewriteTo: ['optModRewriteEnable'], + optUseHttps: null + }; + const optHttpsInfo = { + optPrivateKey: null, + optCertificate: null + }; + console.assert(this); const renderOpts = (opts) => { const _this = this; @@ -253,6 +299,20 @@ class App extends React.Component { this.setState({showAdvanced: !this.state.showAdvanced}) }} >{this.state.showAdvanced ? 'Hide Advanced Options' : 'Show Advanced Options'}) + + const httpsOptions = (() => { + let disable = (!this.webapp || !this.webapp.opts.optUseHttps); + let hasCrypto = this.webapp && (this.webapp.opts.optPrivateKey || this.webapp.opts.optCertificate); + const textBoxes = renderOpts(optHttpsInfo) + return [(
{!disable && textBoxes} + {hasCrypto && !disable && To regenerate, remove key and cert. Be sure to take a copy first, for possible later use!} + {!disable && } +
)]; + })(); + const {state} = this; return (
@@ -316,7 +376,7 @@ class App extends React.Component { {options} {advancedButton} - {state.showAdvanced &&
{advOptions}
} + {state.showAdvanced &&
{advOptions}{httpsOptions}
} diff --git a/react-ui/js/options.js b/react-ui/js/options.js index d6f2af11..98b26163 100644 --- a/react-ui/js/options.js +++ b/react-ui/js/options.js @@ -69,6 +69,7 @@ export function AppOption({disabled, indent, name, value, appOptions, onChange: