improved installer options

This commit is contained in:
wh1te909 2020-08-24 20:10:40 +00:00
parent 7dd9658213
commit 413e0fab9f
8 changed files with 438 additions and 23 deletions

2
.gitignore vendored
View File

@ -32,3 +32,5 @@ app.ini
create_services.py
gen_random.py
sync_salt_modules.py
rmm-*.exe
rmm-*.ps1

View File

@ -1,5 +1,7 @@
from loguru import logger
import os
import subprocess
import tempfile
import zlib
import json
import base64
@ -8,6 +10,7 @@ from packaging import version as pyver
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from rest_framework.decorators import (
api_view,
@ -306,9 +309,109 @@ def install_agent(request):
user=request.user, expiry=dt.timedelta(hours=request.data["expires"])
)
resp = {"token": token, "client": client.pk, "site": site.pk}
resp["showextra"] = True if pyver.parse(version) > pyver.parse("0.10.0") else False
return Response(resp)
if request.data["installMethod"] == "exe":
go_bin = "/usr/local/rmmgo/go/bin/go"
if not os.path.exists(go_bin):
return Response("nogolang", status=status.HTTP_409_CONFLICT)
api = request.data["api"]
atype = request.data["agenttype"]
rdp = request.data["rdp"]
ping = request.data["ping"]
power = request.data["power"]
file_name = f"rmm-{''.join(client.client.lower().split())}-{''.join(site.site.lower().split())}.exe"
exe = os.path.join(settings.EXE_DIR, file_name)
if os.path.exists(exe):
os.remove(exe)
cmd = [
"env",
"GOOS=windows",
"GOARCH=amd64",
go_bin,
"build",
f"-ldflags=\"-X 'main.Version={version}'",
f"-X 'main.Api={api}'",
f"-X 'main.Client={client.pk}'",
f"-X 'main.Site={site.pk}'",
f"-X 'main.Atype={atype}'",
f"-X 'main.Rdp={rdp}'",
f"-X 'main.Ping={ping}'",
f"-X 'main.Power={power}'",
f"-X 'main.Token={token}'\"",
"-o",
exe,
os.path.join(settings.BASE_DIR, "core/installer.go"),
]
r = subprocess.run(" ".join(cmd), shell=True)
if settings.DEBUG:
with open(exe, "rb") as f:
response = HttpResponse(
f.read(),
content_type="application/vnd.microsoft.portable-executable",
)
response["Content-Disposition"] = f"inline; filename={file_name}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={file_name}"
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
return response
elif request.data["installMethod"] == "manual":
resp = {"token": token, "client": client.pk, "site": site.pk}
resp["showextra"] = (
True if pyver.parse(version) > pyver.parse("0.10.0") else False
)
return Response(resp)
elif request.data["installMethod"] == "powershell":
ps = os.path.join(settings.BASE_DIR, "core/installer.ps1")
with open(ps, "r") as f:
text = f.read()
replace_dict = {
"versionchange": request.data["exe"],
"clientchange": str(client.pk),
"sitechange": str(site.pk),
"apichange": request.data["api"],
"atypechange": request.data["agenttype"],
"powerchange": str(request.data["power"]),
"rdpchange": str(request.data["rdp"]),
"pingchange": str(request.data["ping"]),
"downloadchange": request.data["download"],
"tokenchange": token,
}
for i, j in replace_dict.items():
text = text.replace(i, j)
file_name = os.path.join(settings.EXE_DIR, "rmm-installer.ps1")
if os.path.exists(file_name):
os.remove(file_name)
with open(file_name, "w") as f:
f.write(text)
if settings.DEBUG:
with open(file_name, "r") as f:
response = HttpResponse(f.read(), content_type="text/plain",)
response["Content-Disposition"] = f"inline; filename={file_name}"
return response
else:
response = HttpResponse()
response["Content-Disposition"] = f"attachment; filename={file_name}"
response["X-Accel-Redirect"] = f"/private/exe/{file_name}"
return response
return Response(text)
@api_view(["POST"])

View File

@ -0,0 +1,130 @@
package main
import (
"bufio"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"time"
"flag"
"strings"
)
var Version string
var Api string
var Client string
var Site string
var Atype string
var Power string
var Rdp string
var Ping string
var Token string
func downloadAgent(filepath string, url string) (err error) {
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Bad response: %s", resp.Status)
}
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}
func main() {
debugLog := flag.String("log", "", "Verbose output")
localSalt := flag.String("local-salt", "", "Use local salt minion")
localMesh := flag.String("local-mesh", "", "Use local mesh agent")
flag.Parse()
agentBinary := fmt.Sprintf("C:\\Windows\\Temp\\winagent-v%s.exe", Version)
url := fmt.Sprintf("https://github.com/wh1te909/winagent/releases/download/v%s/winagent-v%s.exe", Version, Version)
fmt.Println("Downloading agent...")
dl := downloadAgent(agentBinary, url)
if dl != nil {
fmt.Println("ERROR: unable to download agent from", url)
fmt.Println(dl)
os.Exit(1)
}
fmt.Println("Extracting files...")
winagentCmd := exec.Command(agentBinary, "/VERYSILENT", "/SUPPRESSMSGBOXES")
err := winagentCmd.Run()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
time.Sleep(20 * time.Second)
tacrmm := "C:\\Program Files\\TacticalAgent\\tacticalrmm.exe"
cmdArgs := []string{
"-m", "install", "--api", Api, "--client-id",
Client, "--site-id", Site, "--agent-type", Atype,
"--power", Power, "--rdp", Rdp, "--ping", Ping,
"--auth", Token,
}
if strings.TrimSpace(*debugLog) == "DEBUG" {
cmdArgs = append(cmdArgs, "--log", "DEBUG")
}
if len(strings.TrimSpace(*localSalt)) != 0 {
cmdArgs = append(cmdArgs, "--local-salt", *localSalt)
}
if len(strings.TrimSpace(*localMesh)) != 0 {
cmdArgs = append(cmdArgs, "--local-mesh", *localMesh)
}
fmt.Println("Installation starting.")
cmd := exec.Command(tacrmm, cmdArgs...)
cmdReader, err := cmd.StdoutPipe()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
scanner := bufio.NewScanner(cmdReader)
go func() {
for scanner.Scan() {
fmt.Println(scanner.Text())
}
}()
err = cmd.Start()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
err = cmd.Wait()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
}

View File

@ -0,0 +1,40 @@
# author: https://github.com/bradhawkins85
$installversion = 'versionchange'
$api = '"apichange"'
$clientid = 'clientchange'
$siteid = 'sitechange'
$agenttype = '"atypechange"'
$power = 'powerchange'
$rdp = 'rdpchange'
$ping = 'pingchange'
$auth = '"tokenchange"'
$downloadlink = 'downloadchange'
$serviceName = 'tacticalagent'
If (Get-Service $serviceName -ErrorAction SilentlyContinue) {
write-host ('Tactical RMM Is Already Installed')
} Else {
$OutPath = $env:TMP
$output = $installversion
Try
{
Invoke-WebRequest -Uri $downloadlink -OutFile $OutPath\$output
Start-Process -FilePath $OutPath\$output -ArgumentList ('/VERYSILENT /SUPPRESSMSGBOXES') -Wait
write-host ('Extracting...')
Start-Sleep -s 20
Start-Process -FilePath "C:\Program Files\TacticalAgent\tacticalrmm.exe" -ArgumentList ('-m install --api ', "$api", '--client-id', $clientid, '--site-id', $siteid, '--agent-type', "$agenttype", '--power', $power, '--rdp', $rdp, '--ping', $ping, '--auth', "$auth") -Wait
exit 0
}
Catch
{
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
Write-Error -Message "$ErrorMessage $FailedItem"
exit 1
}
Finally
{
Remove-Item -Path $OutPath\$output
}
}

View File

@ -1,5 +1,7 @@
import os
import shutil
import subprocess
import tempfile
from time import sleep
from django.core.management.base import BaseCommand
@ -81,3 +83,21 @@ class Command(BaseCommand):
self.stdout.write("\n")
self.stdout.write(self.style.ERROR("*" * 100))
input("Press Enter to continue...")
# install go
if not os.path.exists("/usr/local/rmmgo/"):
self.stdout.write(self.style.SUCCESS("Installing golang"))
subprocess.run("sudo mkdir -p /usr/local/rmmgo", shell=True)
tmpdir = tempfile.mkdtemp()
r = subprocess.run(
f"wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P {tmpdir}",
shell=True,
)
gotar = os.path.join(tmpdir, "go1.15.linux-amd64.tar.gz")
subprocess.run(f"tar -xzf {gotar} -C {tmpdir}", shell=True)
gofolder = os.path.join(tmpdir, "go")
subprocess.run(f"sudo mv {gofolder} /usr/local/rmmgo/", shell=True)
shutil.rmtree(tmpdir)

View File

@ -1,5 +1,21 @@
#!/bin/bash
SCRIPT_VERSION="1"
SCRIPT_URL='https://raw.githubusercontent.com/wh1te909/tacticalrmm/develop/install.sh'
TMP_FILE=$(mktemp -p "" "rmminstall_XXXXXXXXXX")
curl -s -L "${SCRIPT_URL}" > ${TMP_FILE}
NEW_VER=$(grep "^SCRIPT_VERSION" "$TMP_FILE" | awk -F'[="]' '{print $3}')
if [ "${SCRIPT_VERSION}" \< "${NEW_VER}" ]; then
printf >&2 "${YELLOW}A newer version of this installer script is available.${NC}\n"
printf >&2 "${YELLOW}Please download the latest version from ${GREEN}${SCRIPT_URL}${YELLOW} and re-run.${NC}\n"
rm -f $TMP_FILE
exit 1
fi
rm -f $TMP_FILE
UBU20=$(grep 20.04 "/etc/"*"release")
if ! [[ $UBU20 ]]; then
echo -ne "\033[0;31mThis script will only work on Ubuntu 20.04\e[0m\n"
@ -107,12 +123,24 @@ do
sudo certbot certonly --manual -d *.${rootdomain} --agree-tos --no-bootstrap --manual-public-ip-logging-ok --preferred-challenges dns -m ${letsemail} --no-eff-email
done
print_green 'Creating saltapi user'
sudo adduser --no-create-home --disabled-password --gecos "" saltapi
echo "saltapi:${SALTPW}" | sudo chpasswd
print_green 'Installing golang'
sudo apt install -y curl wget
sudo mkdir -p /usr/local/rmmgo
go_tmp=$(mktemp -d -t rmmgo-XXXXXXXXXX)
wget https://golang.org/dl/go1.15.linux-amd64.tar.gz -P ${go_tmp}
tar -xzf ${go_tmp}/go1.15.linux-amd64.tar.gz -C ${go_tmp}
sudo mv ${go_tmp}/go /usr/local/rmmgo/
rm -rf ${go_tmp}
print_green 'Installing Nginx'
sudo apt install -y nginx
@ -120,8 +148,6 @@ sudo systemctl stop nginx
print_green 'Installing NodeJS'
sudo apt install -y curl wget
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt update
sudo apt install -y gcc g++ make

View File

@ -2,7 +2,7 @@
<q-card style="min-width: 70vw">
<q-card-section class="row">
<q-card-actions align="left">
<div class="text-h6">Installation Instructions</div>
<div class="text-h6">Manual Installation Instructions</div>
</q-card-actions>
<q-space />
<q-card-actions align="right">

View File

@ -59,8 +59,16 @@
Select Version
<q-select dense outlined v-model="version" :options="Object.values(versions)" />
</q-card-section>
<q-card-section>
Installation Method
<div class="q-gutter-sm">
<q-radio v-model="installMethod" val="exe" label="Dynamically generated exe" />
<q-radio v-model="installMethod" val="powershell" label="Powershell" />
<q-radio v-model="installMethod" val="manual" label="Manual" />
</div>
</q-card-section>
<q-card-actions align="left">
<q-btn label="Show Install Command" color="primary" type="submit" />
<q-btn :label="installButtonText" color="primary" type="submit" />
</q-card-actions>
</q-form>
</q-card-section>
@ -88,13 +96,14 @@ export default {
site: null,
version: null,
agenttype: "server",
expires: 1,
expires: 720,
power: false,
rdp: false,
ping: false,
github: [],
showAgentDownload: false,
info: {},
installMethod: "exe",
};
},
methods: {
@ -129,21 +138,90 @@ export default {
const release = this.github.filter(i => i.name === this.version)[0];
const download = release.assets[0].browser_download_url;
const exe = `${release.name}.exe`;
const clientStripped = this.client.replace(/\s/g, "").toLowerCase();
const siteStripped = this.site.replace(/\s/g, "").toLowerCase();
const data = { client: this.client, site: this.site, expires: this.expires, version: this.version };
axios.post("/agents/installagent/", data).then(r => {
this.info = {
exe,
download,
api,
agenttype: this.agenttype,
expires: this.expires,
power: this.power ? 1 : 0,
rdp: this.rdp ? 1 : 0,
ping: this.ping ? 1 : 0,
data: r.data,
};
this.showAgentDownload = true;
const data = {
installMethod: this.installMethod,
client: this.client,
site: this.site,
expires: this.expires,
version: this.version,
agenttype: this.agenttype,
power: this.power ? 1 : 0,
rdp: this.rdp ? 1 : 0,
ping: this.ping ? 1 : 0,
api,
release,
download,
exe,
};
if (this.installMethod === "manual") {
axios.post("/agents/installagent/", data).then(r => {
this.info = {
exe,
download,
api,
agenttype: this.agenttype,
expires: this.expires,
power: this.power ? 1 : 0,
rdp: this.rdp ? 1 : 0,
ping: this.ping ? 1 : 0,
data: r.data,
installMethod: this.installMethod,
};
this.showAgentDownload = true;
});
} else if (this.installMethod === "exe") {
this.$q.loading.show({ message: "Generating executable..." });
const fileName = `rmm-${clientStripped}-${siteStripped}.exe`;
this.$axios
.post("/agents/installagent/", data, { responseType: "blob" })
.then(r => {
this.$q.loading.hide();
const blob = new Blob([r.data], { type: "application/vnd.microsoft.portable-executable" });
let link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
link.click();
this.showDLMessage();
})
.catch(e => {
let err;
switch (e.response.status) {
case 409:
err = "Golang is not installed";
break;
case 412:
err = "Golang build failed";
break;
default:
err = "Something went wrong";
}
this.$q.loading.hide();
this.notifyError(err, 4000);
});
} else if (this.installMethod === "powershell") {
const psName = `rmm-${clientStripped}-${siteStripped}.ps1`;
this.$axios
.post("/agents/installagent/", data, { responseType: "blob" })
.then(({ data }) => {
const blob = new Blob([data], { type: "text/plain" });
let link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = psName;
link.click();
this.showDLMessage();
})
.catch(e => this.notifyError("Something went wrong"));
}
},
showDLMessage() {
this.$q.dialog({
message: `Installer for ${this.client}, ${this.site} (${this.agenttype}) will now be downloaded.
You may reuse this installer for ${this.expires} hours before it expires. No command line arguments are needed.`,
});
},
},
@ -154,6 +232,22 @@ export default {
return this.tree[this.client];
}
},
installButtonText() {
let text;
switch (this.installMethod) {
case "exe":
text = "Generate and download exe";
break;
case "powershell":
text = "Download powershell script";
break;
case "manual":
text = "Show manual installation instructions";
break;
}
return text;
},
},
created() {
this.getClientsSites();