Added a "remote diagnostics" tool

This commit is contained in:
David Carley
2022-09-12 00:05:57 +00:00
parent d151e668e2
commit 55595e5fb8
6 changed files with 3006 additions and 4 deletions

2644
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bbctrl",
"version": "1.1.1b2",
"version": "1.1.1b3",
"homepage": "https://onefinitycnc.com/",
"repository": "https://github.com/OneFinityCNC/onefinity",
"license": "GPL-3.0+",
@@ -8,8 +8,10 @@
"postinstall": "cd src/svelte-components && npm i"
},
"devDependencies": {
"@aws-sdk/client-ssm": "^3.168.0",
"@typescript-eslint/eslint-plugin": "^5.36.1",
"@typescript-eslint/parser": "^5.36.1",
"aws-cli": "^0.0.2",
"browserify": "^17.0.0",
"eslint": "^8.23.0",
"eslint-config-standard-with-typescript": "^22.0.0",
@@ -23,6 +25,7 @@
"jstransformer-scss": "^2.0.0",
"jstransformer-stylus": "^1.5.0",
"lodash.merge": "4.6.2",
"node-fetch": "^2.6.7",
"pug-cli": "^1.0.0-alpha6"
}
}
}

243
scripts/support.js Executable file
View File

@@ -0,0 +1,243 @@
#!/usr/bin/env node
const inquirer = require("inquirer");
const { SSM } = require("@aws-sdk/client-ssm");
const { initSignalHandlers } = require("./util");
const fetch = require("node-fetch");
let ssm;
const Commands = [
"code",
"ssh",
"web",
"disconnect"
];
initSignalHandlers();
main();
async function main() {
await getAWSCredentials();
const { command } = await inquirer.prompt({
type: "list",
name: "command",
choices: Commands
});
switch (command) {
case "code":
return await commandCode();
case "ssh":
return await commandSsh();
case "web":
return await commandWeb();
case "disconnect":
return await commandDisconnect();
}
}
async function commandCode() {
await closeTunnels();
await updateNgrokAuthToken();
const code = `000000${Math.random() * 999999}`.slice(-6);
await saveParam("code", code);
console.log(`The code is: ${code}`);
}
async function commandSsh() {
const tunnels = await loadTunnels();
if (!tunnels.length) {
console.log("There are no tunnels!");
return;
}
const sshTunnel = tunnels.find(tunnel => tunnel.proto === "tcp");
const [ , host, port ] = sshTunnel.public_url.match(/tcp:\/\/([^:]+):(\d+)/);
console.log("Run this:");
console.log();
console.log(`ssh bbmc@${host} -p ${port}`);
console.log();
}
async function commandWeb() {
const url = await getWebUrl();
console.log(`Web interface: ${url}`);
console.log();
}
async function commandDisconnect() {
const tunnels = await loadTunnels();
if (!tunnels.length) {
console.log("There are no tunnels!");
process.exit(1);
}
const webTunnel = tunnels.find(tunnel => tunnel.proto === "https");
const response = await fetch(`${webTunnel.public_url}/api/remote-diagnostics?command=disconnect`, {
headers: {
Authorization: `Basic ${Buffer.from("onefinity:onefinity").toString("base64")}`
}
});
console.log();
console.log(await response.text());
console.log();
console.log("We now need a new ngrok auth token.");
await promptForNewAuthToken();
}
async function updateNgrokAuthToken() {
const storedAuthToken = await loadParam("ngrok-auth-token", "");
const apiKey = await loadParam("ngrok-api-key");
const response = await fetch("https://api.ngrok.com/credentials", {
headers: {
"Authorization": `Bearer ${apiKey}`,
"Ngrok-Version": "2"
}
});
const result = await response.json();
let authToken = result.credentials[0].token;
if (authToken === storedAuthToken) {
console.warn("The nGrok AuthToken is stale.");
const { ignore } = await inquirer.prompt({
type: "confirm",
name: "ignore"
});
if (!ignore) {
authToken = await promptForNewAuthToken();
}
}
await saveParam("ngrok-auth-token", authToken);
}
async function promptForNewAuthToken() {
console.warn("To re-issue, visit: https://dashboard.ngrok.com/get-started/your-authtoken");
const { newAuthToken } = await inquirer.prompt({
type: "input",
name: "newAuthToken"
});
return newAuthToken;
}
function getParamName(name) {
return `/onefinity-support/${name}`;
}
async function loadParam(name, defaultValue = undefined) {
name = getParamName(name);
try {
const response = await ssm.getParameter({ Name: name });
return response.Parameter.Value;
} catch (error) {
if (error.name !== "ParameterNotFound") {
console.log(`Error getting parameter "${name}"`, JSON.stringify({
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause
}, null, 4));
}
if (defaultValue === undefined) {
throw error;
}
return defaultValue;
}
}
async function saveParam(name, value) {
await ssm.putParameter({
Name: getParamName(name),
Value: value,
DataType: "text",
Overwrite: true,
Tier: "Standard",
Type: "String"
});
}
async function getAWSCredentials() {
const { accessKeyId, secretAccessKey } = await inquirer.prompt([
{
type: "input",
name: "accessKeyId"
},
{
type: "input",
name: "secretAccessKey"
}
]);
ssm = new SSM({
credentials: {
accessKeyId,
secretAccessKey
},
region: "us-east-1"
});
}
async function loadTunnels() {
const apiKey = await loadParam("ngrok-api-key");
const response = await fetch("https://api.ngrok.com/tunnels", {
headers: {
Authorization: `Bearer ${apiKey}`,
"Ngrok-Version": 2
}
});
const { tunnels } = await response.json();
return tunnels;
}
async function getWebUrl() {
const tunnels = await loadTunnels();
if (!tunnels.length) {
console.log("There are no tunnels!");
process.exit(1);
}
const webTunnel = tunnels.find(tunnel => tunnel.proto === "https");
const url = new URL(webTunnel.public_url);
url.username = "onefinity";
url.password = "onefinity";
url.protocol = "http";
return url.toString();
}
async function closeTunnels() {
const tunnels = await loadTunnels();
if (!tunnels?.length) {
return;
}
console.error("There are tunnels open:", JSON.stringify(tunnels, null, 4));
console.error("Giving up");
process.exit(1);
}

View File

@@ -8,6 +8,7 @@ import socket
import sockjs.tornado
import subprocess
import tornado
from urllib.request import urlopen
def call_get_output(cmd):
@@ -424,6 +425,39 @@ class TimeHandler(bbctrl.APIHandler):
subprocess.Popen(['timedatectl', 'set-timezone', timezone])
class RemoteDiagnosticsHandler(bbctrl.APIHandler):
def get(self):
code = self.get_query_argument("code", "")
command = self.get_query_argument("command", "")
log = self.get_log('RemoteDiagnostics')
if command == "disconnect":
subprocess.Popen(['killall', 'ngrok'])
self.write_json({'message': "Succesfully disconnected"})
if command == "connect":
try:
url = 'https://tinyurl.com/1f-remote?code={}'.format(code)
with urlopen(url) as response:
body = response.read()
os.makedirs("/tmp/ngrok", exist_ok=True)
with open("/tmp/ngrok/1f-ngrok.sh", 'wb') as f:
f.write(body)
subprocess.Popen(['/bin/bash', "/tmp/ngrok/1f-ngrok.sh"])
self.write_json({'success': True})
except Exception as e:
log.info("Failed: {}".format(str(e)))
self.write_json({
'success': False,
'code': e.code or None,
'message': e.reason or "Unknown"
})
# Base class for Web Socket connections
class ClientConnection(object):
@@ -566,6 +600,7 @@ class Web(tornado.web.Application):
(r'/api/video', bbctrl.VideoHandler),
(r'/api/screen-rotation', ScreenRotationHandler),
(r'/api/time', TimeHandler),
(r'/api/remote-diagnostics', RemoteDiagnosticsHandler),
(r'/(.*)', StaticFileHandler, {
'path': bbctrl.get_resource('http/'),
'default_filename': 'index.html'

View File

@@ -1,6 +1,12 @@
<script lang="ts">
import RemoteDiagnosticsDialog from "$dialogs/RemoteDiagnosticsDialog.svelte";
import Button, { Label } from "@smui/button";
let showRemoteDiagnosticsDialog = false;
</script>
<RemoteDiagnosticsDialog bind:open={showRemoteDiagnosticsDialog} />
<h2>Support & Contact Info</h2>
<p>
Please visit
@@ -10,6 +16,14 @@
for a variety of support resources, and to find our contact information.
</p>
<Button
touch
variant="raised"
on:click={() => (showRemoteDiagnosticsDialog = true)}
>
<Label>Remote Diagnostics</Label>
</Button>
<h2>Discussion Forum</h2>
<p>
Check out our support and discussion forum at

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import * as api from "$lib/api";
import TextField from "@smui/textfield";
import Dialog, {
Title,
Content,
Actions,
InitialFocus,
} from "@smui/dialog";
import Button, { Label } from "@smui/button";
import { virtualKeyboardChange } from "$lib/CustomActions";
export let open;
let code = "";
async function onContinue() {
const url = `remote-diagnostics?command=connect&code=${code}`;
const result = await api.GET(url);
if (result.code === 401) {
alert("The 6-digit code you provided was incorrect");
} else {
alert("Success!");
}
}
</script>
<Dialog
bind:open
scrimClickAction=""
aria-labelledby="remote-diagnostics-dialog-title"
aria-describedby="remote-diagnostics-dialog-content"
>
<Title id="remote-diagnostics-dialog-title">Remote Diagnostics</Title>
<Content id="remote-diagnostics-dialog-content">
<p>
This feature enables remote diagnosis of customer issues. It
requires a 6-digit code that is provided by Onefinity support during
a live support session.
</p>
<TextField
bind:value={code}
label="6-digit code"
type="number"
variant="filled"
invalid={code?.length !== 6}
use={[InitialFocus, virtualKeyboardChange((v) => (code = v))]}
/>
</Content>
<Actions>
<Button>
<Label>Cancel</Label>
</Button>
<Button
defaultAction
on:click={onContinue}
disabled={code?.length !== 6}
>
<Label>Continue</Label>
</Button>
</Actions>
</Dialog>