blob: 9a1b420a25eb9699d57466007d689871d3e63848 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const fs = require('fs-extra');
const http = require('http');
const ngrok = require('ngrok');
const url = require('url');
const util = require('util');
const crypto = require("crypto");
const log = require('../log');
class NgrokAgent {
constructor(argv, invoker) {
this.argv = argv;
this.invoker = invoker;
}
async getAgent(action) {
log.verbose("Setting up ngrok", this.argv.ngrokRegion ? `(region: ${this.argv.ngrokRegion})` : "");
// 1. start local server on random port
this.ngrokServer = http.createServer(this.ngrokHandler.bind(this));
// turn server.listen() into promise so we can await
const listen = util.promisify( this.ngrokServer.listen.bind(this.ngrokServer) );
await listen(0, '127.0.0.1');
// 2. start ngrok tunnel connected to that port
this.ngrokServerPort = this.ngrokServer.address().port;
// create a unique authorization token that we check on our local instance later
// this adds extra protection on top of the uniquely generated ngrok subdomain (e.g. a01ae275.ngrok.io)
this.ngrokAuth = crypto.randomBytes(32).toString("hex");
const ngrokUrl = await ngrok.connect({
addr: this.ngrokServerPort,
region: this.argv.ngrokRegion
});
// 3. pass on public ngrok url to agent
action.parameters.push({
key: "$ngrokUrl",
value: url.parse(ngrokUrl).host
});
action.parameters.push({
key: "$ngrokAuth",
value: this.ngrokAuth
});
const h = log.highlightColor;
log.step(`Ngrok forwarding: ${h(ngrokUrl)} => http://localhost:${h(this.ngrokServerPort)}`);
log.debug(`ngrok agent auth key: ${this.ngrokAuth}`)
return fs.readFileSync(`${__dirname}/../../agent/agent-ngrok.js`, {encoding: 'utf8'});
}
async stop() {
try {
if (this.ngrokServer) {
this.ngrokServer.close();
this.ngrokServer = null;
}
} finally {
await ngrok.kill();
}
}
// local http server retrieving forwards from the ngrok agent, running them
// as a blocking local invocation and then returning the activation result back
ngrokHandler(req, res) {
// check authorization against our unique token
const authHeader = req.headers.authorization;
if (authHeader !== this.ngrokAuth) {
res.statusCode = 401;
res.end();
return;
}
if (req.method === 'POST') {
// agent POSTs arguments as json body
let body = '';
// collect full request body first
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const params = JSON.parse(body);
const activationId = params.$activationId;
delete params.$activationId;
log.verbose(); // because of the .....
log.log();
log.highlight("Activation: ", activationId);
log.verbose("Parameters:", params);
const startTime = Date.now();
const result = await this.invoker.run(params, activationId);
const duration = Date.now() - startTime;
log.succeed(`Completed activation ${activationId} in ` + log.highlightColor(`${duration/1000.0} sec`));
log.verbose("Result:", result);
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(result));
} catch (e) {
log.error(e);
res.statusCode = 400;
res.end();
}
});
} else {
res.statusCode = 404;
res.end();
}
}
}
module.exports = NgrokAgent;