diff --git a/config.js b/config.js index 5a1ceed..410a5c8 100644 --- a/config.js +++ b/config.js @@ -2,13 +2,19 @@ exports.ids = { chrome: [ - 'lmeddoobegbaiopohmpmmobpnpjifpii', // open in Firefox (Chrome) - 'agaecbnjediafcdopcfidcdiponjlmnk', // open in Explorer (Chrome) - 'hhalgjmpmjelidhhjldondajffjbcmcg', // open in Firefox (Opera) - 'poibpkhpegdblnblbkcppknekhkhmmlp', // open in Edge (Chrome) - 'amojccmdnkdlcjcplmkijeenigbhfbpd', // open in Opera (Chrome) + 'lmeddoobegbaiopohmpmmobpnpjifpii', // Open in Firefox (Chrome) + 'hhalgjmpmjelidhhjldondajffjbcmcg', // Open in Firefox (Opera) + 'agaecbnjediafcdopcfidcdiponjlmnk', // Open in IE (Chrome) + 'poibpkhpegdblnblbkcppknekhkhmmlp', // Open in Edge (Chrome) + 'jjlkfjglmelpbkgfeebcdipalggpapag', // Open in Edge (Opera) + 'amojccmdnkdlcjcplmkijeenigbhfbpd', // Open in Opera (Chrome) + 'eoenobhieedpimobhembbgdcdhdcdcig', // Open in Safari (Chrome) + 'noedpljikmfalclpadmnjbabbignpfge', // Open in IE (Opera) + 'ocnfecjfebnllnapjjoncgjnnkfmobjc', // Media Converter (Chrome) + 'fabccabfpmdadbhljpcmcbmepepfllnb', // Media Converter (Opera) ], firefox: [ - '{5610edea-88c1-4370-b93d-86aa131971d1}', // open in Explorer + '{5610edea-88c1-4370-b93d-86aa131971d1}', // Open in IE + '{0ff128a1-c286-4e73-bffa-9ae879b244d5}', // Media Converter ] }; diff --git a/follow-redirects.js b/follow-redirects.js new file mode 100644 index 0000000..4f8ea10 --- /dev/null +++ b/follow-redirects.js @@ -0,0 +1,208 @@ +'use strict'; +var url = require('url'); +var assert = require('assert'); +var http = require('http'); +var https = require('https'); +var Writable = require('stream').Writable; + +var nativeProtocols = {'http:': http, 'https:': https}; +var schemes = {}; +var exports = module.exports = { + maxRedirects: 21 +}; +// RFC7231§4.2.1: Of the request methods defined by this specification, +// the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. +var safeMethods = {GET: true, HEAD: true, OPTIONS: true, TRACE: true}; + +// Create handlers that pass events from native requests +var eventHandlers = Object.create(null); +['abort', 'aborted', 'error', 'socket'].forEach(function (event) { + eventHandlers[event] = function (arg) { + this._redirectable.emit(event, arg); + }; +}); + +// An HTTP(S) request that can be redirected +function RedirectableRequest(options, responseCallback) { + // Initialize the request + Writable.call(this); + this._options = options; + this._redirectCount = 0; + this._bufferedWrites = []; + + // Attach a callback if passed + if (responseCallback) { + this.on('response', responseCallback); + } + + // React to responses of native requests + var self = this; + this._onNativeResponse = function (response) { + self._processResponse(response); + }; + + // Perform the first request + this._performRequest(); +} +RedirectableRequest.prototype = Object.create(Writable.prototype); + +// Executes the next native request (initial or redirect) +RedirectableRequest.prototype._performRequest = function () { + // If specified, use the agent corresponding to the protocol + // (HTTP and HTTPS use different types of agents) + var protocol = this._options.protocol; + if (this._options.agents) { + this._options.agent = this._options.agents[schemes[protocol]]; + } + + // Create the native request + var nativeProtocol = nativeProtocols[this._options.protocol]; + var request = this._currentRequest = + nativeProtocol.request(this._options, this._onNativeResponse); + this._currentUrl = url.format(this._options); + + // Set up event handlers + request._redirectable = this; + for (var event in eventHandlers) { + if (event) { + request.on(event, eventHandlers[event]); + } + } + + // End a redirected request + // (The first request must be ended explicitly with RedirectableRequest#end) + if (this._currentResponse) { + // If no body was written to the original request, or the method was changed to GET, + // end the redirected request (without writing a body). + var bufferedWrites = this._bufferedWrites; + if (bufferedWrites.length === 0 || this._options.method === 'GET') { + request.end(); + // The body of the original request must be added to the redirected request. + } else { + var i = 0; + (function writeNext() { + if (i < bufferedWrites.length) { + var bufferedWrite = bufferedWrites[i++]; + request.write(bufferedWrite.data, bufferedWrite.encoding, writeNext); + } else { + request.end(); + } + })(); + } + } +}; + +// Processes a response from the current native request +RedirectableRequest.prototype._processResponse = function (response) { + // RFC7231§6.4: The 3xx (Redirection) class of status code indicates + // that further action needs to be taken by the user agent in order to + // fulfill the request. If a Location header field is provided, + // the user agent MAY automatically redirect its request to the URI + // referenced by the Location field value, + // even if the specific status code is not understood. + var location = response.headers.location; + if (location && this._options.followRedirects !== false && + response.statusCode >= 300 && response.statusCode < 400) { + // RFC7231§6.4: A client SHOULD detect and intervene + // in cyclical redirections (i.e., "infinite" redirection loops). + if (++this._redirectCount > this._options.maxRedirects) { + return this.emit('error', new Error('Max redirects exceeded.')); + } + + // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates + // that the target resource resides temporarily under a different URI + // and the user agent MUST NOT change the request method + // if it performs an automatic redirection to that URI. + if (response.statusCode !== 307) { + // RFC7231§6.4: Automatic redirection needs to done with + // care for methods not known to be safe […], + // since the user might not wish to redirect an unsafe request. + if (!(this._options.method in safeMethods)) { + this._options.method = 'GET'; + } + } + + // Perform the redirected request + var redirectUrl = url.resolve(this._currentUrl, location); + Object.assign(this._options, url.parse(redirectUrl)); + this._currentResponse = response; + this._performRequest(); + } else { + // The response is not a redirect; return it as-is + response.responseUrl = this._currentUrl; + this.emit('response', response); + + // Clean up + delete this._options; + delete this._bufferedWrites; + } +}; + +// Aborts the current native request +RedirectableRequest.prototype.abort = function () { + this._currentRequest.abort(); +}; + +// Flushes the headers of the current native request +RedirectableRequest.prototype.flushHeaders = function () { + this._currentRequest.flushHeaders(); +}; + +// Sets the noDelay option of the current native request +RedirectableRequest.prototype.setNoDelay = function (noDelay) { + this._currentRequest.setNoDelay(noDelay); +}; + +// Sets the socketKeepAlive option of the current native request +RedirectableRequest.prototype.setSocketKeepAlive = function (enable, initialDelay) { + this._currentRequest.setSocketKeepAlive(enable, initialDelay); +}; + +// Sets the timeout option of the current native request +RedirectableRequest.prototype.setTimeout = function (timeout, callback) { + this._currentRequest.setTimeout(timeout, callback); +}; + +// Writes buffered data to the current native request +RedirectableRequest.prototype._write = function (data, encoding, callback) { + this._currentRequest.write(data, encoding, callback); + this._bufferedWrites.push({data: data, encoding: encoding}); +}; + +// Ends the current native request +RedirectableRequest.prototype.end = function (data, encoding, callback) { + this._currentRequest.end(data, encoding, callback); + if (data) { + this._bufferedWrites.push({data: data, encoding: encoding}); + } +}; + +// Export a redirecting wrapper for each native protocol +Object.keys(nativeProtocols).forEach(function (protocol) { + var scheme = schemes[protocol] = protocol.substr(0, protocol.length - 1); + var nativeProtocol = nativeProtocols[protocol]; + var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); + + // Executes an HTTP request, following redirects + wrappedProtocol.request = function (options, callback) { + if (typeof options === 'string') { + options = url.parse(options); + options.maxRedirects = exports.maxRedirects; + } else { + options = Object.assign({ + maxRedirects: exports.maxRedirects, + protocol: protocol + }, options); + } + assert.equal(options.protocol, protocol, 'protocol mismatch'); + + return new RedirectableRequest(options, callback); + }; + + // Executes a GET request, following redirects + wrappedProtocol.get = function (options, callback) { + var request = wrappedProtocol.request(options, callback); + request.end(); + return request; + }; +}); diff --git a/host.js b/host.js index 7e7522c..89a9727 100755 --- a/host.js +++ b/host.js @@ -1,16 +1,59 @@ 'use strict'; +function lazyRequire (lib, name) { + if (!name) { + name = lib; + } + global.__defineGetter__(name, function () { + return require(lib); + }); + return global[name]; +} + var spawn = require('child_process').spawn; +var fs = lazyRequire('fs'); +var os = lazyRequire('os'); +var path = lazyRequire('path'); +var http = lazyRequire('./follow-redirects').http; +var https = lazyRequire('./follow-redirects').https; + +var server, files = []; var config = { - version: '0.1.3', - isWin: /^win/.test(process.platform) + version: '0.1.6' }; +// closing node when parent process is killed +process.stdin.resume(); +process.stdin.on('end', () => { + files.forEach(file => { + try { + fs.unlink(file); + } + catch (e) {} + }); + try { + server.close(); + server.unref(); + } + catch (e) {} + process.exit(); +}); + +/////////////////process.on('uncaughtException', e => console.error(e)); function observe (msg, push, done) { if (msg.cmd === 'version') { push({ - version: config.version + version: config.version, + }); + done(); + } + if (msg.cmd === 'spec') { + push({ + version: config.version, + env: process.env, + separator: path.sep, + tmpdir: os.tmpdir() }); done(); } @@ -19,16 +62,84 @@ function observe (msg, push, done) { done(); } else if (msg.cmd === 'spawn') { - let sp = spawn(msg.command, msg.arguments || [], msg.properties || {}); + if (msg.env) { + msg.env.forEach(n => process.env.PATH += path.delimiter + n); + } + let sp = spawn(msg.command, msg.arguments || [], Object.assign({env: process.env}, msg.properties)); sp.stdout.on('data', stdout => push({stdout})); sp.stderr.on('data', stderr => push({stderr})); sp.on('close', (code) => { - push({code}); + push({ + cmd: msg.cmd, + code + }); + done(); + }); + sp.on('error', e => { + push({ + code: 1007, + error: e.message + }); + done(); + }); + } + else if (msg.cmd === 'clean-tmp') { + files.forEach(file => { + try { + fs.unlink(file); + } + catch (e) {} + }); + files = []; + push({ + code: 0 + }); + done(); + } + else if (msg.cmd === 'ifup') { + server = http.createServer(function (req, res) { + if (req.headers['api-key'] !== msg.key) { + res.statusCode = 400; + return res.end('HTTP/1.1 400 Bad API Key. Restarting application may fix this.'); + } + if (req.method === 'PUT') { + let filename = req.headers['file-path']; + files.push(filename); + let file = fs.createWriteStream(filename); + req.pipe(file); + file.on('finish', () => { + file.close(() => { + res.statusCode = 200; + res.end('File is stored locally'); + }); + }); + file.on('error', (e) => { + console.error(e); + res.statusCode = 400; + res.end('HTTP/1.1 400 Bad Request'); + }); + } + }); + server.on('error', (e) => { + push({ + error: e.message, + code: 1006 + }); + done(); + }); + server.listen(msg.port, () => { + push({ + code: 0, + msg: 'Server listening on: http://localhost:' + msg.port + }); done(); }); } else if (msg.cmd === 'exec') { - let sp = spawn(msg.command, msg.arguments || [], msg.properties || {}); + if (msg.env) { + msg.env.forEach(n => process.env.PATH += path.delimiter + n); + } + let sp = spawn(msg.command, msg.arguments || [], Object.assign({env: process.env}, msg.properties)); let stderr = '', stdout = ''; sp.stdout.on('data', data => stdout += data); sp.stderr.on('data', data => stderr += data); @@ -41,12 +152,68 @@ function observe (msg, push, done) { done(); }); } + else if (msg.cmd === 'dir') { + fs.readdir(msg.path, (error, files) => { + if (error) { + push({ + error: `Cannot open directory; number=${error.errno}, code=${error.code}`, + code: 1002 + }); + } + else { + push({ + files, + separator: path.sep + }); + } + done(); + }); + } else if (msg.cmd === 'env') { push({ env: process.env }); done(); } + else if (msg.cmd === 'download') { + let file = fs.createWriteStream(msg.filepath); + let request = https.get({ + hostname: msg.hostname, + port: msg.port, + path: msg.path + }, (response) => { + let size = parseInt(response.headers['content-length'], 10); + response.pipe(file); + file.on('finish', () => { + if (msg.chmod) { + fs.chmodSync(msg.filepath, msg.chmod); + } + file.close(() => { + let s = fs.statSync(msg.filepath).size; + push({ + size, + path: msg.filepath, + code: s === size ? 0 : 1004, + error: s !== size ? `file-size (${s}) does not match the header content-length (${size}). + Link: ${msg.hostname}/${msg.path}` : null + }); + done(); + }); + }); + }); + request.on('error', (err) => { + fs.unlink(msg.filepath); + push({ + error: err.message, + code: 1001 + }); + done(); + }); + request.on('socket', function (socket) { + socket.setTimeout(msg.timeout || 60000); + socket.on('timeout', () => request.abort()); + }); + } else { push({ error: 'cmd is unknown', diff --git a/linux/app/follow-redirects.js b/linux/app/follow-redirects.js new file mode 120000 index 0000000..532c425 --- /dev/null +++ b/linux/app/follow-redirects.js @@ -0,0 +1 @@ +../../follow-redirects.js \ No newline at end of file diff --git a/linux/app/install.js b/linux/app/install.js index 63fb438..7e19584 100644 --- a/linux/app/install.js +++ b/linux/app/install.js @@ -82,6 +82,7 @@ function application (callback) { } fs.createReadStream('host.js').pipe(fs.createWriteStream(path.join(dir, 'host.js'))); fs.createReadStream('messaging.js').pipe(fs.createWriteStream(path.join(dir, 'messaging.js'))); + fs.createReadStream('follow-redirects.js').pipe(fs.createWriteStream(path.join(dir, 'follow-redirects.js'))); callback(); }); }); diff --git a/mac/app/follow-redirects.js b/mac/app/follow-redirects.js new file mode 120000 index 0000000..532c425 --- /dev/null +++ b/mac/app/follow-redirects.js @@ -0,0 +1 @@ +../../follow-redirects.js \ No newline at end of file diff --git a/mac/app/install.js b/mac/app/install.js index ece6d95..fb1bb80 100644 --- a/mac/app/install.js +++ b/mac/app/install.js @@ -85,6 +85,8 @@ function application (callback) { } fs.createReadStream('host.js').pipe(fs.createWriteStream(path.join(dir, 'host.js'))); fs.createReadStream('messaging.js').pipe(fs.createWriteStream(path.join(dir, 'messaging.js'))); + fs.createReadStream('follow-redirects.js').pipe(fs.createWriteStream(path.join(dir, 'follow-redirects.js'))); + callback(); }); }); diff --git a/windows/app/follow-redirects.js b/windows/app/follow-redirects.js new file mode 120000 index 0000000..532c425 --- /dev/null +++ b/windows/app/follow-redirects.js @@ -0,0 +1 @@ +../../follow-redirects.js \ No newline at end of file diff --git a/windows/app/install.js b/windows/app/install.js index c3dfef9..ce196a9 100644 --- a/windows/app/install.js +++ b/windows/app/install.js @@ -50,9 +50,12 @@ function application (callback) { if (e) { throw e; } - fs.createReadStream('..\\node.exe').pipe(fs.createWriteStream(path.join(dir, 'node.exe'))); fs.createReadStream('host.js').pipe(fs.createWriteStream(path.join(dir, 'host.js'))); fs.createReadStream('messaging.js').pipe(fs.createWriteStream(path.join(dir, 'messaging.js'))); + fs.createReadStream('follow-redirects.js').pipe(fs.createWriteStream(path.join(dir, 'follow-redirects.js'))); + try { + fs.createReadStream('..\\node.exe').pipe(fs.createWriteStream(path.join(dir, 'node.exe'))); + } catch (e) {} callback(); }); }