Initial commit
This commit is contained in:
commit
528a22ae7d
|
@ -0,0 +1,2 @@
|
||||||
|
.idea/
|
||||||
|
*.iml
|
|
@ -0,0 +1,79 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
urlPath = "/url/open"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RemoteClient struct {
|
||||||
|
client *http.Client
|
||||||
|
baseUrl string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(host, token string) *RemoteClient {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RemoteClient{client: client, baseUrl: "https://" + host, token: token}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteClient) OpenURL(urlStr string) error {
|
||||||
|
formData := url.Values{
|
||||||
|
"url": {urlStr},
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _, err := r.doRequest(urlPath, formData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if status != http.StatusOK {
|
||||||
|
return errors.New("invalid status: " + strconv.Itoa(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteClient) doRequest(path string, v url.Values) (int, []byte, error) {
|
||||||
|
encoded := v.Encode()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, r.baseUrl + path, strings.NewReader(encoded))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer " + r.token)
|
||||||
|
req.Header.Set("Content-Length", strconv.Itoa(len(encoded)))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
res, err := r.client.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(res.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.StatusCode, b, nil
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
module meow.tf/streamdeck-remote
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.0 // indirect
|
||||||
|
github.com/valyala/fastjson v1.4.1
|
||||||
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
|
||||||
|
meow.tf/streamdeck/sdk v0.0.0-20190519021527-54a933f8777d
|
||||||
|
)
|
|
@ -0,0 +1,14 @@
|
||||||
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/valyala/fastjson v1.4.1 h1:hrltpHpIpkaxll8QltMU8c3QZ5+qIiCL8yKqPFJI/yE=
|
||||||
|
github.com/valyala/fastjson v1.4.1/go.mod h1:nV6MsjxL2IMJQUoHDIrjEI7oLyeqK6aBD7EFWPsvP8o=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
|
||||||
|
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
meow.tf/streamdeck/sdk v0.0.0-20190519021527-54a933f8777d h1:PPZHRoZFy9p4GjXssLvTneJfX6cS0bEm51md5TqXFgU=
|
||||||
|
meow.tf/streamdeck/sdk v0.0.0-20190519021527-54a933f8777d/go.mod h1:SnrBz5Bcdgk/wwIvgjo+3gXiBYV6b/dYSpb2AFxjHcA=
|
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"Actions": [
|
||||||
|
{
|
||||||
|
"Icon": "ssh",
|
||||||
|
"Name": "SSH",
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "ssh",
|
||||||
|
"TitleAlignment": "bottom",
|
||||||
|
"FontSize": "16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SupportedInMultiActions": false,
|
||||||
|
"Tooltip": "Execute a command over SSH",
|
||||||
|
"UUID": "tf.meow.remote.ssh",
|
||||||
|
"PropertyInspectorPath": "index_pi.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Icon": "website",
|
||||||
|
"Name": "Website",
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "website",
|
||||||
|
"TitleAlignment": "bottom",
|
||||||
|
"FontSize": "16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SupportedInMultiActions": false,
|
||||||
|
"Tooltip": "Open a website on a remote computer",
|
||||||
|
"UUID": "tf.meow.remote.website",
|
||||||
|
"PropertyInspectorPath": "index_pi_server.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Author": "Meow.tf",
|
||||||
|
"Category": "Remote",
|
||||||
|
"CodePathWin": "remote.exe",
|
||||||
|
"Description": "Control remote systems over SSH or Daemon, executing commands, opening URLs, etc.",
|
||||||
|
"PropertyInspectorPath": "index_pi.html",
|
||||||
|
"Name": "Remote",
|
||||||
|
"Icon": "pluginIcon",
|
||||||
|
"URL": "https://www.elgato.com/gaming/stream-deck",
|
||||||
|
"Version": "1.2",
|
||||||
|
"OS": [
|
||||||
|
{
|
||||||
|
"Platform": "windows",
|
||||||
|
"MinimumVersion" : "10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 568 B |
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,66 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,minimal-ui,viewport-fit=cover">
|
||||||
|
<meta name=apple-mobile-web-app-capable content=yes>
|
||||||
|
<meta name=apple-mobile-web-app-status-bar-style content=black>
|
||||||
|
<title>Property Inspector Samples PI</title>
|
||||||
|
<link rel="stylesheet" href="css/sdpi.css">
|
||||||
|
<!--link rel="stylesheet"
|
||||||
|
media="screen and (max-width: 1025px)"
|
||||||
|
href="css/local.css" -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="sdpi-wrapper">
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="host" title="IP or Hostname of the server, with optional port.">
|
||||||
|
<div class="sdpi-item-label">SSH IP/Host</div>
|
||||||
|
<input class="sdpi-item-value" id="ssh_host" value="" placeholder="e.g. 192.168.61.1:21" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="user" title="SSH Username">
|
||||||
|
<div class="sdpi-item-label">Username</div>
|
||||||
|
<input class="sdpi-item-value" id="ssh_user" value="" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="ssh_password_container">
|
||||||
|
<div class="sdpi-item-label">Password</div>
|
||||||
|
<input type="password" id="ssh_password" class="sdpi-item-value" value="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="my_private_file_selector">
|
||||||
|
<div class="sdpi-item-label">SSH Key</div>
|
||||||
|
<div class="sdpi-item-group file" id="filepickergroup">
|
||||||
|
<input class="sdpi-item-value" type="file" id="ssh_key">
|
||||||
|
<label class="sdpi-file-info" for="ssh_key">no file...</label>
|
||||||
|
<label class="sdpi-file-label" for="ssh_key">Choose file...</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="ssh_key_passphrase_container" style="display: none;">
|
||||||
|
<div class="sdpi-item-label">Key Passphrase</div>
|
||||||
|
<input type="password" id="ssh_key_passphrase" class="sdpi-item-value" value="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="command">
|
||||||
|
<div class="sdpi-item-label">Command</div>
|
||||||
|
<input class="sdpi-item-value" id="ssh_command" value="" placeholder="" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <script src="echomd.js"></script> -->
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||||
|
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/node-forge@0.7.0/dist/forge.min.js"></script>
|
||||||
|
<script src="index_pi.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,274 @@
|
||||||
|
// this is our global websocket, used to communicate from/to Stream Deck software
|
||||||
|
// and some info about our plugin, as sent by Stream Deck software
|
||||||
|
var websocket = null,
|
||||||
|
uuid = null,
|
||||||
|
actionInfo = {},
|
||||||
|
settings = {},
|
||||||
|
isQT = navigator.appVersion.includes('QtWebEngine'); // 'oninput'; // change this, if you want interactive elements act on any change, or while they're modified
|
||||||
|
|
||||||
|
function connectSocket (
|
||||||
|
inPort,
|
||||||
|
inUUID,
|
||||||
|
inMessageType,
|
||||||
|
inApplicationInfo,
|
||||||
|
inActionInfo
|
||||||
|
) {
|
||||||
|
connectElgatoStreamDeckSocket(
|
||||||
|
inPort,
|
||||||
|
inUUID,
|
||||||
|
inMessageType,
|
||||||
|
inApplicationInfo,
|
||||||
|
inActionInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectElgatoStreamDeckSocket (inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||||
|
uuid = inUUID;
|
||||||
|
// please note: the incoming arguments are of type STRING, so
|
||||||
|
// in case of the inActionInfo, we must parse it into JSON first
|
||||||
|
actionInfo = JSON.parse(inActionInfo); // cache the info
|
||||||
|
inInfo = JSON.parse(inInfo);
|
||||||
|
websocket = new WebSocket('ws://localhost:' + inPort);
|
||||||
|
|
||||||
|
/** Since the PI doesn't have access to your OS native settings
|
||||||
|
* Stream Deck sends some color settings to PI
|
||||||
|
* We use these to adjust some styles (e.g. highlight-colors for checkboxes)
|
||||||
|
*/
|
||||||
|
addDynamicStyles(inInfo.colors, 'connectElgatoStreamDeckSocket');
|
||||||
|
|
||||||
|
/** let's see, if we have some settings */
|
||||||
|
settings = getPropFromString(actionInfo, 'payload.settings');
|
||||||
|
console.log(settings, actionInfo);
|
||||||
|
initPropertyInspector(5);
|
||||||
|
|
||||||
|
// if connection was established, the websocket sends
|
||||||
|
// an 'onopen' event, where we need to register our PI
|
||||||
|
websocket.onopen = function () {
|
||||||
|
var json = {
|
||||||
|
event: inRegisterEvent,
|
||||||
|
uuid: inUUID
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.send(JSON.stringify(json));
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.onmessage = function (evt) {
|
||||||
|
// Received message from Stream Deck
|
||||||
|
var jsonObj = JSON.parse(evt.data);
|
||||||
|
var event = jsonObj['event'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPropertyInspector(initDelay) {
|
||||||
|
if (actionInfo['action'] == 'tf.meow.remote.website') {
|
||||||
|
$('#url_container').show();
|
||||||
|
$('#url_background_container').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(settings).forEach(function (item) {
|
||||||
|
$('#' + item).val(settings[item]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('input').each(function() {
|
||||||
|
var $this = $(this),
|
||||||
|
id = $this.attr('id');
|
||||||
|
|
||||||
|
let $item = $this.closest('.sdpi-item');
|
||||||
|
|
||||||
|
$this.on('change', function(e) {
|
||||||
|
const type = $this.attr('type');
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
const info = $item.find('.sdpi-file-info');
|
||||||
|
|
||||||
|
if (info) {
|
||||||
|
const s = decodeURIComponent($this.val().replace(/^C:\\fakepath\\/, '')).split('/').pop();
|
||||||
|
|
||||||
|
info.text(s.length > 28
|
||||||
|
? s.substr(0, 10)
|
||||||
|
+ '...'
|
||||||
|
+ s.substr(s.length - 10, s.length)
|
||||||
|
: s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let val = $this.val();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'checkbox':
|
||||||
|
if (val == 'false' || val == 'true') {
|
||||||
|
val = (/^true$/i).test(val);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSetting(id, val);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#ssh_key').change(function(e) {
|
||||||
|
if (e.target.files.length < 1) {
|
||||||
|
// Hide passphrase field
|
||||||
|
$('#ssh_key_passphrase_container').hide();
|
||||||
|
$('#ssh_password_container').show();
|
||||||
|
} else {
|
||||||
|
$('#ssh_password_container').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
var f = e.target.files[0];
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
// Closure to capture the file information.
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var result = e.target.result;
|
||||||
|
|
||||||
|
var pki = forge.pki;
|
||||||
|
|
||||||
|
try {
|
||||||
|
pki.privateKeyFromPem(result);
|
||||||
|
$('#ssh_key_passphrase_container').hide();
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex.message.indexOf('PEM is encrypted.') !== -1) {
|
||||||
|
$('#ssh_key_passphrase_container').show();
|
||||||
|
} else {
|
||||||
|
// Show error message?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read in the image file as a data URL.
|
||||||
|
reader.readAsText(f);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isQT) {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initPropertyInspector(100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stream Deck software passes system-highlight color information
|
||||||
|
* to Property Inspector. Here we 'inject' the CSS styles into the DOM
|
||||||
|
* when we receive this information. */
|
||||||
|
function addDynamicStyles (clrs, fromWhere) {
|
||||||
|
const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style');
|
||||||
|
if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100);
|
||||||
|
const clr = clrs.highlightColor.slice(0, 7);
|
||||||
|
const clr1 = fadeColor(clr, 100);
|
||||||
|
const clr2 = fadeColor(clr, 60);
|
||||||
|
const metersActiveColor = fadeColor(clr, -60);
|
||||||
|
|
||||||
|
node.setAttribute('id', 'sdpi-dynamic-styles');
|
||||||
|
node.innerHTML = `
|
||||||
|
|
||||||
|
input[type="radio"]:checked + label span,
|
||||||
|
input[type="checkbox"]:checked + label span {
|
||||||
|
background-color: ${clrs.highlightColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:active:checked + label span,
|
||||||
|
input[type="radio"]:active + label span,
|
||||||
|
input[type="checkbox"]:active:checked + label span,
|
||||||
|
input[type="checkbox"]:active + label span {
|
||||||
|
background-color: ${clrs.mouseDownColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:active + label span,
|
||||||
|
input[type="checkbox"]:active + label span {
|
||||||
|
background-color: ${clrs.buttonPressedBorderColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
td.selected,
|
||||||
|
td.selected:hover,
|
||||||
|
li.selected:hover,
|
||||||
|
li.selected {
|
||||||
|
color: white;
|
||||||
|
background-color: ${clrs.highlightColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.sdpi-file-label > label:active,
|
||||||
|
.sdpi-file-label.file:active,
|
||||||
|
label.sdpi-file-label:active,
|
||||||
|
label.sdpi-file-info:active,
|
||||||
|
input[type="file"]::-webkit-file-upload-button:active,
|
||||||
|
button:active {
|
||||||
|
background-color: ${clrs.buttonPressedBackgroundColor};
|
||||||
|
color: ${clrs.buttonPressedTextColor};
|
||||||
|
border-color: ${clrs.buttonPressedBorderColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-progress-value,
|
||||||
|
meter::-webkit-meter-optimum-value {
|
||||||
|
background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2})
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-progress-value:active,
|
||||||
|
meter::-webkit-meter-optimum-value:active {
|
||||||
|
background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr})
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.body.appendChild(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** UTILITIES */
|
||||||
|
|
||||||
|
/** get a JSON property from a (dot-separated) string
|
||||||
|
* Works on nested JSON, e.g.:
|
||||||
|
* jsn = {
|
||||||
|
* propA: 1,
|
||||||
|
* propB: 2,
|
||||||
|
* propC: {
|
||||||
|
* subA: 3,
|
||||||
|
* subB: {
|
||||||
|
* testA: 5,
|
||||||
|
* testB: 'Hello'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* getPropFromString(jsn,'propC.subB.testB') will return 'Hello';
|
||||||
|
*/
|
||||||
|
const getPropFromString = (jsn, str, sep = '.') => {
|
||||||
|
const arr = str.split(sep);
|
||||||
|
return arr.reduce((obj, key) =>
|
||||||
|
(obj && obj.hasOwnProperty(key)) ? obj[key] : undefined, jsn);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account)
|
||||||
|
Usage:
|
||||||
|
fadeColor('#061261', 100); // will lighten the color
|
||||||
|
fadeColor('#200867'), -100); // will darken the color
|
||||||
|
*/
|
||||||
|
function fadeColor (col, amt) {
|
||||||
|
const min = Math.min, max = Math.max;
|
||||||
|
const num = parseInt(col.replace(/#/g, ''), 16);
|
||||||
|
const r = min(255, max((num >> 16) + amt, 0));
|
||||||
|
const g = min(255, max((num & 0x0000FF) + amt, 0));
|
||||||
|
const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0));
|
||||||
|
return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSetting(setting, value) {
|
||||||
|
if (!settings) {
|
||||||
|
settings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
settings[setting] = value;
|
||||||
|
|
||||||
|
setSettings(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSettings(settings) {
|
||||||
|
var json = {
|
||||||
|
"event": "setSettings",
|
||||||
|
"context": uuid,
|
||||||
|
"payload": settings
|
||||||
|
};
|
||||||
|
|
||||||
|
if (websocket) {
|
||||||
|
websocket.send(JSON.stringify(json));
|
||||||
|
} else {
|
||||||
|
console.log('Update:', json);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,minimal-ui,viewport-fit=cover">
|
||||||
|
<meta name=apple-mobile-web-app-capable content=yes>
|
||||||
|
<meta name=apple-mobile-web-app-status-bar-style content=black>
|
||||||
|
<title>StreamDeck Remote - Server</title>
|
||||||
|
<link rel="stylesheet" href="css/sdpi.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="sdpi-wrapper">
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="host" title="IP or Hostname of the server, with optional port.">
|
||||||
|
<div class="sdpi-item-label">Host</div>
|
||||||
|
<input class="sdpi-item-value" id="remote_host" value="" placeholder="e.g. 192.168.61.1:21" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="ssh_password_container">
|
||||||
|
<div class="sdpi-item-label">Token</div>
|
||||||
|
<input type="text" id="remote_token" class="sdpi-item-value" value="" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sdpi-item" id="url_container" style="display: none;">
|
||||||
|
<div class="sdpi-item-label">URL</div>
|
||||||
|
<input type="text" id="url" class="sdpi-item-value" value="" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="url_background_container" type="checkbox" class="sdpi-item" style="display: none;">
|
||||||
|
<div class="sdpi-item-label">Options</div>
|
||||||
|
<input class="sdpi-item-value" id="background" type="checkbox" value="true">
|
||||||
|
<label for="background"><span></span>Access in background</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <script src="echomd.js"></script> -->
|
||||||
|
<script
|
||||||
|
src="https://code.jquery.com/jquery-3.3.1.min.js"
|
||||||
|
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="index_pi.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,31 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"meow.tf/streamdeck/sdk"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sshAction = "tf.meow.remote.ssh"
|
||||||
|
websiteAction = "tf.meow.remote.website"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
f, err := os.Create("log.txt")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
log.SetOutput(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk.RegisterAction(sshAction, sshActionHandler)
|
||||||
|
sdk.RegisterAction(websiteAction, serverActionHandler(websiteActionHandler))
|
||||||
|
|
||||||
|
err = sdk.Open()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sdk.Wait()
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/valyala/fastjson"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"meow.tf/streamdeck-remote/client"
|
||||||
|
"meow.tf/streamdeck/sdk"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverActionFunc func(context, host, token string, settings *fastjson.Value)
|
||||||
|
|
||||||
|
func serverActionHandler(f serverActionFunc) sdk.ActionHandler{
|
||||||
|
return func(action, context string, payload *fastjson.Value, deviceId string) {
|
||||||
|
settings := payload.Get("settings")
|
||||||
|
|
||||||
|
host := sdk.JsonStringValue(settings, "remote_host")
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h, p, err := net.SplitHostPort(host)
|
||||||
|
|
||||||
|
// Ensure there's a host + port
|
||||||
|
if err != nil {
|
||||||
|
h = host
|
||||||
|
p = "4443"
|
||||||
|
}
|
||||||
|
|
||||||
|
host = net.JoinHostPort(h, p)
|
||||||
|
|
||||||
|
log.Println("Using host", host)
|
||||||
|
|
||||||
|
token := sdk.JsonStringValue(settings, "remote_token")
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f(context, host, token, settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func websiteActionHandler(context, host, token string, settings *fastjson.Value) {
|
||||||
|
c := client.New(host, token)
|
||||||
|
|
||||||
|
url := sdk.JsonStringValue(settings, "url")
|
||||||
|
|
||||||
|
if url == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
background := settings.GetBool("background")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if background {
|
||||||
|
// Use Go to visit URL
|
||||||
|
res, err := http.Get(url)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(ioutil.Discard, res.Body)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = c.OpenURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error opening url:", err)
|
||||||
|
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
} else {
|
||||||
|
sdk.ShowOk(context)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/valyala/fastjson"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"io/ioutil"
|
||||||
|
"meow.tf/streamdeck/sdk"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sshActionHandler(action, context string, payload *fastjson.Value, deviceId string) {
|
||||||
|
settings := payload.Get("settings")
|
||||||
|
|
||||||
|
if settings == nil {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := sdk.JsonStringValue(settings, "ssh_user")
|
||||||
|
|
||||||
|
host := sdk.JsonStringValue(settings, "ssh_host")
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := sdk.JsonStringValue(settings, "ssh_command")
|
||||||
|
|
||||||
|
if command == "" {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyFile := sdk.JsonStringValue(settings, "ssh_key"); keyFile != "" {
|
||||||
|
s, err := loadPrivateKey(keyFile, sdk.JsonStringValue(settings, "ssh_key_passphrase"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Auth = []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(s),
|
||||||
|
}
|
||||||
|
} else if password := sdk.JsonStringValue(settings, "ssh_password"); password != "" {
|
||||||
|
config.Auth = []ssh.AuthMethod{
|
||||||
|
ssh.Password(password),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ssh.Dial("tcp", host, config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := client.NewSession()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = session.Run(command)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
sdk.ShowOk(context)
|
||||||
|
} else {
|
||||||
|
sdk.ShowAlert(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPrivateKey(file, passphrase string) (ssh.Signer, error) {
|
||||||
|
f, err := os.Open(file)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(f)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if passphrase != "" {
|
||||||
|
return ssh.ParsePrivateKeyWithPassphrase(b, []byte(passphrase))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ssh.ParsePrivateKey(b)
|
||||||
|
}
|
Loading…
Reference in New Issue