release: Mark VIII 1.0
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
param(
|
||||
[string]$PagerHost = "172.16.52.1",
|
||||
[string]$User = "root",
|
||||
[string]$Password = "",
|
||||
[string]$SshKey = "",
|
||||
[string]$BuildDir = "",
|
||||
[switch]$NoPortalRefresh
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$PayloadKey = "pager-webui"
|
||||
$PayloadCategory = "remote_access"
|
||||
$PayloadDir = Join-Path $Root "payload\user\$PayloadCategory\$PayloadKey"
|
||||
if (-not (Test-Path $PayloadDir)) { throw "Payload dir not found: $PayloadDir" }
|
||||
|
||||
if (-not $BuildDir) { $BuildDir = Join-Path $Root "build" }
|
||||
$OutDir = Join-Path $BuildDir $PayloadKey
|
||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||
|
||||
# --- 1. Stage payload tree -------------------------------------------------
|
||||
$Stage = Join-Path $OutDir "stage"
|
||||
if (Test-Path $Stage) { Remove-Item -Recurse -Force $Stage }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Stage "user\$PayloadCategory") | Out-Null
|
||||
Copy-Item -Recurse $PayloadDir (Join-Path $Stage "user\$PayloadCategory\$PayloadKey")
|
||||
|
||||
# --- 2. Build zip (portal format: payload-<b64>.zip) -----------------------
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($PayloadKey)).TrimEnd('=').Replace('+','-').Replace('/','_')
|
||||
$ZipName = "payload-$b64.zip"
|
||||
$ZipPath = Join-Path $OutDir $ZipName
|
||||
if (Test-Path $ZipPath) { Remove-Item -Force $ZipPath }
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
$zip = New-Object System.IO.Compression.ZipArchive([IO.File]::Open($ZipPath, 'Create'), [IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
Get-ChildItem -Recurse -File $Stage | Where-Object {
|
||||
$_.FullName -notmatch '[\\/]__pycache__[\\/]' -and $_.Extension -ne '.pyc'
|
||||
} | ForEach-Object {
|
||||
$rel = $_.FullName.Substring($Stage.Length + 1).Replace('\', '/')
|
||||
$entry = $zip.CreateEntry($rel, [IO.Compression.CompressionLevel]::Optimal)
|
||||
$es = $entry.Open()
|
||||
$bytes = [IO.File]::ReadAllBytes($_.FullName)
|
||||
if ($rel -match '(^|/)(payload\.sh|pagerwebui\.init)$') {
|
||||
$bytes = [byte[]]($bytes | Where-Object { $_ -ne 13 })
|
||||
}
|
||||
$es.Write($bytes, 0, $bytes.Length)
|
||||
$es.Close()
|
||||
}
|
||||
} finally { $zip.Dispose() }
|
||||
|
||||
# --- 3. Manifest with generated fields ------------------------------------
|
||||
$hash = (Get-FileHash -Algorithm SHA256 $ZipPath).Hash.ToLower()
|
||||
$manifest = Get-Content -Raw (Join-Path $PayloadDir "_hak5_manifest.json") | ConvertFrom-Json
|
||||
$manifest.time = [int64]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
|
||||
$manifest.last_hash = $hash
|
||||
$manifest.zip = $ZipName
|
||||
$manifest = $manifest | ConvertTo-Json
|
||||
Set-Content -Path (Join-Path $OutDir "_hak5_manifest.json") -Value $manifest -Encoding ascii
|
||||
Write-Host "Built: $ZipPath"
|
||||
|
||||
# --- 4. Credentials / transport -------------------------------------------
|
||||
if ($SshKey) {
|
||||
$sshBase = "$User@$PagerHost"
|
||||
$scp = "scp -i `"$SshKey`""
|
||||
$ssh = "ssh -i `"$SshKey`""
|
||||
} elseif (Get-Command sshpass -ErrorAction SilentlyContinue) {
|
||||
if (-not $Password) { $Password = Read-Host -AsSecureString "Pager root password"; $Password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)) }
|
||||
$sshBase = "$User@$PagerHost"
|
||||
$scp = "sshpass -p `"$Password`" scp"
|
||||
$ssh = "sshpass -p `"$Password`" ssh"
|
||||
} else {
|
||||
Write-Host "`nNo sshpass or -SshKey found. Run these manually (password prompts appear):"
|
||||
Write-Host " scp `"$ZipPath`" $User@${PagerHost}:/tmp/"
|
||||
Write-Host " ssh $User@$PagerHost `"cd /root/payloads && unzip -q -o /tmp/$ZipName && chmod +x user/general/$PayloadKey/payload.sh && rm -f /tmp/$ZipName`""
|
||||
Write-Host "Then re-run this script with -SshKey, or install sshpass."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 5. Upload + extract on device ----------------------------------------
|
||||
& cmd /c "$scp `"$ZipPath`" `"$(Join-Path $OutDir '_hak5_manifest.json')`" ${sshBase}:/tmp/" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed" }
|
||||
|
||||
$remotePayloadDir = "user/$PayloadCategory/$PayloadKey"
|
||||
$legacyPayloadDir = "user/general/$PayloadKey"
|
||||
$remoteCmd = "cd /root/payloads && rm -rf $remotePayloadDir $legacyPayloadDir && unzip -q -o /tmp/$ZipName && cp /tmp/_hak5_manifest.json $remotePayloadDir/_hak5_manifest.json && chmod +x $remotePayloadDir/payload.sh && chmod -R 755 $remotePayloadDir/www && rm -f /tmp/$ZipName /tmp/_hak5_manifest.json && if [ -x /etc/init.d/pagerwebui ] && /etc/init.d/pagerwebui running >/dev/null 2>&1; then /etc/init.d/pagerwebui restart; fi && echo EXTRACT_OK"
|
||||
& cmd /c "$ssh $sshBase `"$remoteCmd`""
|
||||
if ($LASTEXITCODE -ne 0) { throw "Remote extraction failed" }
|
||||
Write-Host "Installed to /root/payloads/$remotePayloadDir/"
|
||||
|
||||
# --- 6. Portal refresh (best-effort) --------------------------------------
|
||||
if (-not $NoPortalRefresh) {
|
||||
if (-not $Password) {
|
||||
Write-Host "Skipping portal refresh (no password supplied). Run the payload from the on-device menu to verify."
|
||||
} else {
|
||||
# Base64 the remote command so cmd/ssh quoting cannot mangle the JSON body.
|
||||
$loginCmd = "curl -s -X POST http://127.0.0.1:1471/api/login -d '{""username"":""root"",""password"":""$Password""}'"
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($loginCmd))
|
||||
$tokenJson = & cmd /c "$ssh $sshBase `"echo $b64 | base64 -d | sh`""
|
||||
$tokenJson = [string]$tokenJson -replace '\x1b\[[0-9;?]*[A-Za-z]', ''
|
||||
$token = ($tokenJson | ConvertFrom-Json).token
|
||||
if ($token) {
|
||||
$refreshCmd = "curl -fsS -X POST http://127.0.0.1:1471/api/payloads/portal/refresh -H 'Authorization: Bearer $token'"
|
||||
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($refreshCmd))
|
||||
& cmd /c "$ssh $sshBase `"echo $b64 | base64 -d | sh`"" | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Portal refreshed. The payload should appear in the on-device Payloads menu / Virtual Pager portal."
|
||||
} else {
|
||||
Write-Warning "Portal refresh failed; the local payload installation is still complete."
|
||||
}
|
||||
} else {
|
||||
Write-Host "Login to portal refresh failed; the payload is installed as a directory - run it from the menu."
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Host "Deploy complete. Run payload.sh from the Pager menu, then browse http://${PagerHost}:8080/"
|
||||
@@ -0,0 +1,19 @@
|
||||
param(
|
||||
[string]$PagerHost = "172.16.52.1",
|
||||
[int]$Port = 8000,
|
||||
[switch]$Tunnel
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe"
|
||||
if (-not (Test-Path $Python)) { throw "Python 3.11 not found. Install with: winget install Python.Python.3.11" }
|
||||
|
||||
if ($Tunnel) {
|
||||
Write-Host "Opening SSH tunnel 1471 -> 127.0.0.1:1471 (for direct terminal WS). Close the ssh window to stop it."
|
||||
Start-Process ssh -ArgumentList "-N","-L","1471:127.0.0.1:1471","root@$PagerHost" -WindowStyle Minimized
|
||||
}
|
||||
|
||||
Write-Host "Dev server: http://127.0.0.1:$Port (API proxied to http://$PagerHost:8080)"
|
||||
Write-Host "Requires the backend deployed: scripts\deploy.ps1"
|
||||
& $Python "$PSScriptRoot\dev_proxy.py" --pager-host $PagerHost --port $Port --www "$Root\payload\user\remote_access\pager-webui\www"
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Local dev proxy: serves www/ statically and proxies /api/* to the Pager."""
|
||||
import argparse
|
||||
import http.server
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--pager-host', default='172.16.52.1')
|
||||
ap.add_argument('--pager-port', type=int, default=8080)
|
||||
ap.add_argument('--port', type=int, default=8000)
|
||||
ap.add_argument('--www', default='payload/user/remote_access/pager-webui/www')
|
||||
args = ap.parse_args()
|
||||
api_target = 'http://%s:%d' % (args.pager_host, args.pager_port)
|
||||
|
||||
class Proxy(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, directory=args.www, **kw)
|
||||
|
||||
def log_message(self, fmt, *a):
|
||||
pass
|
||||
|
||||
def _serve_dev_config(self):
|
||||
body = ('window.PAGER_CONFIG = { apiBase: "", wsBase: "", '
|
||||
'terminalWs: "ws://%s:1471/api/terminal/openWs", '
|
||||
'pagerScreenWs: "ws://%s:1471/api/pager/display/screen.ws", '
|
||||
'pagerKeysWs: "ws://%s:1471/api/pager/input/keys.ws" };\n'
|
||||
% (args.pager_host, args.pager_host, args.pager_host)).encode()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'text/javascript')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Cache-Control', 'no-cache')
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return True
|
||||
|
||||
def _proxy(self, method):
|
||||
body = None
|
||||
if self.headers.get('Content-Length'):
|
||||
body = self.rfile.read(int(self.headers['Content-Length']))
|
||||
req = urllib.request.Request(api_target + self.path, data=body, method=method)
|
||||
if self.headers.get('Cookie'):
|
||||
req.add_header('Cookie', self.headers['Cookie'])
|
||||
req.add_header('Content-Type', self.headers.get('Content-Type', 'application/json'))
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=25) as r:
|
||||
data = r.read()
|
||||
self.send_response(r.status)
|
||||
self.send_header('Content-Type', r.headers.get('Content-Type', 'application/octet-stream'))
|
||||
self.send_header('Content-Length', str(len(data)))
|
||||
for sc in r.headers.get_all('Set-Cookie', []):
|
||||
self.send_header('Set-Cookie', sc)
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
except urllib.error.HTTPError as e:
|
||||
data = e.read()
|
||||
self.send_response(e.code)
|
||||
self.send_header('Content-Type', e.headers.get('Content-Type', 'application/json'))
|
||||
self.send_header('Content-Length', str(len(data)))
|
||||
for sc in e.headers.get_all('Set-Cookie', []):
|
||||
self.send_header('Set-Cookie', sc)
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/js/config.js':
|
||||
return self._serve_dev_config()
|
||||
if self.path.startswith('/api/'):
|
||||
return self._proxy('GET')
|
||||
return super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
return self._proxy('POST') if self.path.startswith('/api/') else self.send_error(404)
|
||||
|
||||
def do_DELETE(self):
|
||||
return self._proxy('DELETE') if self.path.startswith('/api/') else self.send_error(404)
|
||||
|
||||
http.server.ThreadingHTTPServer(('127.0.0.1', args.port), Proxy).serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user