From 413e0fab9f7e9a943e86781a7cc0d69581d6e8c1 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Mon, 24 Aug 2020 20:10:40 +0000 Subject: [PATCH] improved installer options --- .gitignore | 2 + api/tacticalrmm/agents/views.py | 109 ++++++++++++++- api/tacticalrmm/core/installer.go | 130 ++++++++++++++++++ api/tacticalrmm/core/installer.ps1 | 40 ++++++ .../management/commands/post_update_tasks.py | 20 +++ install.sh | 32 ++++- .../modals/agents/AgentDownload.vue | 2 +- .../components/modals/agents/InstallAgent.vue | 126 ++++++++++++++--- 8 files changed, 438 insertions(+), 23 deletions(-) create mode 100644 api/tacticalrmm/core/installer.go create mode 100644 api/tacticalrmm/core/installer.ps1 diff --git a/.gitignore b/.gitignore index c21eed81..6b30a234 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ app.ini create_services.py gen_random.py sync_salt_modules.py +rmm-*.exe +rmm-*.ps1 diff --git a/api/tacticalrmm/agents/views.py b/api/tacticalrmm/agents/views.py index 63465db2..1bdc86f6 100644 --- a/api/tacticalrmm/agents/views.py +++ b/api/tacticalrmm/agents/views.py @@ -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"]) diff --git a/api/tacticalrmm/core/installer.go b/api/tacticalrmm/core/installer.go new file mode 100644 index 00000000..220ad839 --- /dev/null +++ b/api/tacticalrmm/core/installer.go @@ -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 + } +} diff --git a/api/tacticalrmm/core/installer.ps1 b/api/tacticalrmm/core/installer.ps1 new file mode 100644 index 00000000..c2a46f56 --- /dev/null +++ b/api/tacticalrmm/core/installer.ps1 @@ -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 + } +} \ No newline at end of file diff --git a/api/tacticalrmm/core/management/commands/post_update_tasks.py b/api/tacticalrmm/core/management/commands/post_update_tasks.py index e8807065..882183aa 100644 --- a/api/tacticalrmm/core/management/commands/post_update_tasks.py +++ b/api/tacticalrmm/core/management/commands/post_update_tasks.py @@ -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) diff --git a/install.sh b/install.sh index 69f09da5..dbdeb8f1 100755 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/web/src/components/modals/agents/AgentDownload.vue b/web/src/components/modals/agents/AgentDownload.vue index 6c145731..2796fe10 100644 --- a/web/src/components/modals/agents/AgentDownload.vue +++ b/web/src/components/modals/agents/AgentDownload.vue @@ -2,7 +2,7 @@ -
Installation Instructions
+
Manual Installation Instructions
diff --git a/web/src/components/modals/agents/InstallAgent.vue b/web/src/components/modals/agents/InstallAgent.vue index 58b11a44..6a6adf88 100644 --- a/web/src/components/modals/agents/InstallAgent.vue +++ b/web/src/components/modals/agents/InstallAgent.vue @@ -59,8 +59,16 @@ Select Version
+ + Installation Method +
+ + + +
+
- + @@ -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();