Added a "remote diagnostics" tool
This commit is contained in:
2644
package-lock.json
generated
2644
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
243
scripts/support.js
Executable 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);
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user