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",
|
"name": "bbctrl",
|
||||||
"version": "1.1.1b2",
|
"version": "1.1.1b3",
|
||||||
"homepage": "https://onefinitycnc.com/",
|
"homepage": "https://onefinitycnc.com/",
|
||||||
"repository": "https://github.com/OneFinityCNC/onefinity",
|
"repository": "https://github.com/OneFinityCNC/onefinity",
|
||||||
"license": "GPL-3.0+",
|
"license": "GPL-3.0+",
|
||||||
@@ -8,8 +8,10 @@
|
|||||||
"postinstall": "cd src/svelte-components && npm i"
|
"postinstall": "cd src/svelte-components && npm i"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-ssm": "^3.168.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||||
"@typescript-eslint/parser": "^5.36.1",
|
"@typescript-eslint/parser": "^5.36.1",
|
||||||
|
"aws-cli": "^0.0.2",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"eslint": "^8.23.0",
|
"eslint": "^8.23.0",
|
||||||
"eslint-config-standard-with-typescript": "^22.0.0",
|
"eslint-config-standard-with-typescript": "^22.0.0",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"jstransformer-scss": "^2.0.0",
|
"jstransformer-scss": "^2.0.0",
|
||||||
"jstransformer-stylus": "^1.5.0",
|
"jstransformer-stylus": "^1.5.0",
|
||||||
"lodash.merge": "4.6.2",
|
"lodash.merge": "4.6.2",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
"pug-cli": "^1.0.0-alpha6"
|
"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 sockjs.tornado
|
||||||
import subprocess
|
import subprocess
|
||||||
import tornado
|
import tornado
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
|
||||||
def call_get_output(cmd):
|
def call_get_output(cmd):
|
||||||
@@ -424,6 +425,39 @@ class TimeHandler(bbctrl.APIHandler):
|
|||||||
subprocess.Popen(['timedatectl', 'set-timezone', timezone])
|
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
|
# Base class for Web Socket connections
|
||||||
class ClientConnection(object):
|
class ClientConnection(object):
|
||||||
|
|
||||||
@@ -566,6 +600,7 @@ class Web(tornado.web.Application):
|
|||||||
(r'/api/video', bbctrl.VideoHandler),
|
(r'/api/video', bbctrl.VideoHandler),
|
||||||
(r'/api/screen-rotation', ScreenRotationHandler),
|
(r'/api/screen-rotation', ScreenRotationHandler),
|
||||||
(r'/api/time', TimeHandler),
|
(r'/api/time', TimeHandler),
|
||||||
|
(r'/api/remote-diagnostics', RemoteDiagnosticsHandler),
|
||||||
(r'/(.*)', StaticFileHandler, {
|
(r'/(.*)', StaticFileHandler, {
|
||||||
'path': bbctrl.get_resource('http/'),
|
'path': bbctrl.get_resource('http/'),
|
||||||
'default_filename': 'index.html'
|
'default_filename': 'index.html'
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import RemoteDiagnosticsDialog from "$dialogs/RemoteDiagnosticsDialog.svelte";
|
||||||
|
import Button, { Label } from "@smui/button";
|
||||||
|
|
||||||
|
let showRemoteDiagnosticsDialog = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<RemoteDiagnosticsDialog bind:open={showRemoteDiagnosticsDialog} />
|
||||||
|
|
||||||
<h2>Support & Contact Info</h2>
|
<h2>Support & Contact Info</h2>
|
||||||
<p>
|
<p>
|
||||||
Please visit
|
Please visit
|
||||||
@@ -10,6 +16,14 @@
|
|||||||
for a variety of support resources, and to find our contact information.
|
for a variety of support resources, and to find our contact information.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
touch
|
||||||
|
variant="raised"
|
||||||
|
on:click={() => (showRemoteDiagnosticsDialog = true)}
|
||||||
|
>
|
||||||
|
<Label>Remote Diagnostics</Label>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<h2>Discussion Forum</h2>
|
<h2>Discussion Forum</h2>
|
||||||
<p>
|
<p>
|
||||||
Check out our support and discussion forum at
|
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