Add -f FILE flag support for session and service commands
All 38+ implementations now support: - session -f FILE: Upload files to /tmp/ in session container - service -f FILE: Upload files to /tmp/ in service container - service --bootstrap-file FILE: Read bootstrap script from file
This commit is contained in:
parent
a5a4f23594
commit
c6813e5a18
40 changed files with 2506 additions and 329 deletions
26
CLAUDE.md
26
CLAUDE.md
|
|
@ -117,6 +117,32 @@ The pattern `((VAR++))` returns exit code 1 when VAR is 0. Use `VAR=$((VAR + 1))
|
|||
### Shebang lines
|
||||
Shebang MUST be on line 1, not buried in license headers.
|
||||
|
||||
## Keeping Implementations in Sync
|
||||
|
||||
**CRITICAL: ALL 38 implementations must have feature parity.**
|
||||
|
||||
When adding a new feature to the CLI (e.g., new flag, new command):
|
||||
1. Update the canonical C implementation at `~/git/unsandbox.com/cli/un.c`
|
||||
2. Update ALL 38 implementations in this repo - not just "main" ones, ALL of them
|
||||
3. Use the Task agent to batch update if needed
|
||||
|
||||
Current implementations (ALL must be updated):
|
||||
```
|
||||
un.awk un.clj un.cob un.cpp un.cr un.d un.dart un.erl un.ex un.f90
|
||||
un.forth un.fs un.go un.groovy un.hs un.jl un.js un.kt un.lisp un.lua
|
||||
un.m un.ml un.nim un.php un.pl un.pro un.ps1 un.py un.r un.raku
|
||||
un.rb un.rs un.scm un.sh un.tcl un.ts un.v un.zig
|
||||
```
|
||||
|
||||
### Feature Checklist
|
||||
|
||||
Each implementation must support:
|
||||
- **Execute**: `un file.py` - run code with `-e ENV=val`, `-f FILE`, `-n MODE`, `-a` artifacts
|
||||
- **Session**: `un session` - interactive shell with `-f FILE`, `--tmux`, `--screen`, `--list`, `--attach`, `--kill`
|
||||
- **Service**: `un service` - persistent services with `-f FILE`, `--name`, `--ports`, `--bootstrap`, `--bootstrap-file`, `--list`, `--info`, `--logs`, `--destroy`
|
||||
|
||||
The `-f FILE` flag must work for ALL three commands (execute, session, service) - files go to `/tmp/` in the container.
|
||||
|
||||
## Related Repos
|
||||
|
||||
- `~/git/unsandbox.com/` - Portal (contains un.c CLI at cli/un.c)
|
||||
|
|
|
|||
25
Un.java
25
Un.java
|
|
@ -210,6 +210,19 @@ public class Un {
|
|||
payload.put("vcpu", args.vcpu);
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if (args.files != null && !args.files.isEmpty()) {
|
||||
List<Map<String, String>> inputFiles = new ArrayList<>();
|
||||
for (String filepath : args.files) {
|
||||
byte[] content = Files.readAllBytes(Paths.get(filepath));
|
||||
Map<String, String> fileObj = new HashMap<>();
|
||||
fileObj.put("filename", Paths.get(filepath).getFileName().toString());
|
||||
fileObj.put("content_base64", Base64.getEncoder().encodeToString(content));
|
||||
inputFiles.add(fileObj);
|
||||
}
|
||||
payload.put("input_files", inputFiles);
|
||||
}
|
||||
|
||||
System.out.println(YELLOW + "Creating session..." + RESET);
|
||||
Map<String, Object> result = apiRequest("/sessions", "POST", payload, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Session created: " + result.getOrDefault("id", "N/A") + RESET);
|
||||
|
|
@ -341,6 +354,18 @@ public class Un {
|
|||
if (args.serviceBootstrap != null) {
|
||||
payload.put("bootstrap", args.serviceBootstrap);
|
||||
}
|
||||
// Add input files
|
||||
if (args.files != null && !args.files.isEmpty()) {
|
||||
List<Map<String, String>> inputFiles = new ArrayList<>();
|
||||
for (String filepath : args.files) {
|
||||
byte[] content = Files.readAllBytes(Paths.get(filepath));
|
||||
Map<String, String> fileObj = new HashMap<>();
|
||||
fileObj.put("filename", Paths.get(filepath).getFileName().toString());
|
||||
fileObj.put("content_base64", Base64.getEncoder().encodeToString(content));
|
||||
inputFiles.add(fileObj);
|
||||
}
|
||||
payload.put("input_files", inputFiles);
|
||||
}
|
||||
if (args.network != null) {
|
||||
payload.put("network", args.network);
|
||||
}
|
||||
|
|
|
|||
155
un.awk
155
un.awk
|
|
@ -326,7 +326,90 @@ function service_dump_bootstrap(id, dump_file , endpoint, json_body, timestam
|
|||
}
|
||||
}
|
||||
|
||||
function service_create(name, ports, domains, service_type, bootstrap , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
function read_and_base64(filepath , cmd, b64) {
|
||||
cmd = "base64 -w0 '" filepath "' 2>/dev/null || base64 '" filepath "'"
|
||||
cmd | getline b64
|
||||
close(cmd)
|
||||
return b64
|
||||
}
|
||||
|
||||
function build_input_files_json(files_str , n, files, i, fname, b64, json) {
|
||||
if (files_str == "") return ""
|
||||
n = split(files_str, files, ",")
|
||||
json = ",\"input_files\":["
|
||||
for (i = 1; i <= n; i++) {
|
||||
fname = files[i]
|
||||
b64 = read_and_base64(fname)
|
||||
if (i > 1) json = json ","
|
||||
# Get just the basename for filename
|
||||
cmd = "basename '" fname "'"
|
||||
cmd | getline basename
|
||||
close(cmd)
|
||||
json = json "{\"filename\":\"" escape_json(basename) "\",\"content\":\"" b64 "\"}"
|
||||
}
|
||||
json = json "]"
|
||||
return json
|
||||
}
|
||||
|
||||
function session_create(shell, network, vcpu, input_files , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, line, response, input_files_json) {
|
||||
get_api_keys()
|
||||
|
||||
# Build JSON payload
|
||||
json = "{\"shell\":\"" (shell != "" ? shell : "bash") "\""
|
||||
|
||||
if (network != "") {
|
||||
json = json ",\"network\":\"" escape_json(network) "\""
|
||||
}
|
||||
|
||||
if (vcpu != "") {
|
||||
json = json ",\"vcpu\":" vcpu
|
||||
}
|
||||
|
||||
# Add input_files if provided
|
||||
input_files_json = build_input_files_json(input_files)
|
||||
if (input_files_json != "") {
|
||||
json = json input_files_json
|
||||
}
|
||||
|
||||
json = json "}"
|
||||
|
||||
# Write to temp file
|
||||
tmp = "/tmp/un_awk_sess_" PROCINFO["pid"] ".json"
|
||||
print json > tmp
|
||||
close(tmp)
|
||||
|
||||
# Build HMAC signature if secret key exists
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:/sessions:" json
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
|
||||
# Call curl
|
||||
cmd = "curl -s -X POST '" API_BASE "/sessions' " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||
sig_headers \
|
||||
"-d '@" tmp "'"
|
||||
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
response = response line
|
||||
}
|
||||
close(cmd)
|
||||
|
||||
# Clean up
|
||||
system("rm -f " tmp)
|
||||
|
||||
print YELLOW "Session created (WebSocket required)" RESET
|
||||
print response
|
||||
}
|
||||
|
||||
function service_create(name, ports, domains, service_type, bootstrap, bootstrap_file, input_files , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd, boot_content, line, input_files_json) {
|
||||
get_api_keys()
|
||||
|
||||
# Build JSON payload
|
||||
|
|
@ -355,6 +438,29 @@ function service_create(name, ports, domains, service_type, bootstrap , json,
|
|||
json = json ",\"bootstrap\":\"" escape_json(bootstrap) "\""
|
||||
}
|
||||
|
||||
if (bootstrap_file != "") {
|
||||
# Read file content
|
||||
boot_content = ""
|
||||
while ((getline line < bootstrap_file) > 0) {
|
||||
if (boot_content != "") boot_content = boot_content "\n"
|
||||
boot_content = boot_content line
|
||||
}
|
||||
close(bootstrap_file)
|
||||
|
||||
if (boot_content == "") {
|
||||
print RED "Error: Bootstrap file not found or empty: " bootstrap_file RESET > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
|
||||
json = json ",\"bootstrap_content\":\"" escape_json(boot_content) "\""
|
||||
}
|
||||
|
||||
# Add input_files if provided
|
||||
input_files_json = build_input_files_json(input_files)
|
||||
if (input_files_json != "") {
|
||||
json = json input_files_json
|
||||
}
|
||||
|
||||
json = json "}"
|
||||
|
||||
# Write to temp file
|
||||
|
|
@ -495,12 +601,17 @@ function show_help() {
|
|||
print "Usage: awk -f un.awk <source_file>"
|
||||
print " awk -f un.awk session --list"
|
||||
print " awk -f un.awk session --kill ID"
|
||||
print " awk -f un.awk session [-s SHELL] [-f FILE]..."
|
||||
print " awk -f un.awk key [--extend]"
|
||||
print " awk -f un.awk service --list"
|
||||
print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD]"
|
||||
print " awk -f un.awk service --create --name NAME [--ports PORTS] [--domains DOMAINS] [--type TYPE] [--bootstrap CMD] [-f FILE]..."
|
||||
print " awk -f un.awk service --destroy ID"
|
||||
print " awk -f un.awk service --dump-bootstrap ID [--dump-file FILE]"
|
||||
print ""
|
||||
print "Session options:"
|
||||
print " -s, --shell SHELL Shell to use (default: bash)"
|
||||
print " -f FILE Input file to upload (can be repeated)"
|
||||
print ""
|
||||
print "Service options:"
|
||||
print " --name NAME Service name (required for --create)"
|
||||
print " --ports PORTS Comma-separated port numbers"
|
||||
|
|
@ -509,6 +620,7 @@ function show_help() {
|
|||
print " --bootstrap CMD Bootstrap command or script"
|
||||
print " --dump-bootstrap ID Dump bootstrap script from service"
|
||||
print " --dump-file FILE Save bootstrap to file (with --dump-bootstrap)"
|
||||
print " -f FILE Input file to upload (can be repeated)"
|
||||
print ""
|
||||
print "Requires: UNSANDBOX_API_KEY environment variable"
|
||||
}
|
||||
|
|
@ -536,7 +648,33 @@ END {
|
|||
} else if (ARGC >= 4 && ARGV[2] == "--kill") {
|
||||
session_kill(ARGV[3])
|
||||
} else {
|
||||
print "Usage: awk -f un.awk session --list|--kill ID"
|
||||
# Parse session creation arguments
|
||||
shell = ""
|
||||
network = ""
|
||||
vcpu = ""
|
||||
input_files = ""
|
||||
|
||||
i = 2
|
||||
while (i < ARGC) {
|
||||
if ((ARGV[i] == "--shell" || ARGV[i] == "-s") && i + 1 < ARGC) {
|
||||
shell = ARGV[i + 1]
|
||||
i += 2
|
||||
} else if (ARGV[i] == "-n" && i + 1 < ARGC) {
|
||||
network = ARGV[i + 1]
|
||||
i += 2
|
||||
} else if (ARGV[i] == "-v" && i + 1 < ARGC) {
|
||||
vcpu = ARGV[i + 1]
|
||||
i += 2
|
||||
} else if (ARGV[i] == "-f" && i + 1 < ARGC) {
|
||||
if (input_files != "") input_files = input_files ","
|
||||
input_files = input_files ARGV[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
session_create(shell, network, vcpu, input_files)
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
|
@ -568,6 +706,8 @@ END {
|
|||
domains = ""
|
||||
service_type = ""
|
||||
bootstrap = ""
|
||||
bootstrap_file = ""
|
||||
input_files = ""
|
||||
|
||||
i = 3
|
||||
while (i < ARGC) {
|
||||
|
|
@ -586,6 +726,13 @@ END {
|
|||
} else if (ARGV[i] == "--bootstrap" && i + 1 < ARGC) {
|
||||
bootstrap = ARGV[i + 1]
|
||||
i += 2
|
||||
} else if (ARGV[i] == "--bootstrap-file" && i + 1 < ARGC) {
|
||||
bootstrap_file = ARGV[i + 1]
|
||||
i += 2
|
||||
} else if (ARGV[i] == "-f" && i + 1 < ARGC) {
|
||||
if (input_files != "") input_files = input_files ","
|
||||
input_files = input_files ARGV[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
|
|
@ -596,7 +743,7 @@ END {
|
|||
exit 1
|
||||
}
|
||||
|
||||
service_create(name, ports, domains, service_type, bootstrap)
|
||||
service_create(name, ports, domains, service_type, bootstrap, bootstrap_file, input_files)
|
||||
} else {
|
||||
print "Usage: awk -f un.awk service --list|--create|--destroy ID"
|
||||
}
|
||||
|
|
|
|||
153
un.clj
153
un.clj
|
|
@ -95,6 +95,21 @@
|
|||
(str/replace "\\\"" "\"")
|
||||
(str/replace "\\\\" "\\")))
|
||||
|
||||
(defn read-and-base64 [filepath]
|
||||
(let [content (slurp filepath)
|
||||
bytes (.getBytes content "UTF-8")]
|
||||
(.encodeToString (java.util.Base64/getEncoder) bytes)))
|
||||
|
||||
(defn build-input-files-json [files]
|
||||
(if (empty? files)
|
||||
""
|
||||
(let [file-jsons (map (fn [f]
|
||||
(let [basename (-> (io/file f) .getName)
|
||||
b64 (read-and-base64 f)]
|
||||
(str "{\"filename\":\"" (escape-json basename) "\",\"content\":\"" b64 "\"}")))
|
||||
files)]
|
||||
(str ",\"input_files\":[" (str/join "," file-jsons) "]"))))
|
||||
|
||||
(defn extract-field [field json-str]
|
||||
(let [pattern-str (re-pattern (str "\"" field "\":\"([^\"]*)\""))
|
||||
pattern-num (re-pattern (str "\"" field "\":(\\d+)"))]
|
||||
|
|
@ -235,7 +250,7 @@
|
|||
(flush)))
|
||||
(System/exit exit-code)))))
|
||||
|
||||
(defn session-command [action sid shell network vcpu]
|
||||
(defn session-command [action sid shell network vcpu input-files]
|
||||
(let [api-key (get-api-key)]
|
||||
(case action
|
||||
:list (println (curl-get api-key "/sessions"))
|
||||
|
|
@ -245,11 +260,12 @@
|
|||
:create (let [sh (or shell "bash")
|
||||
network-json (if network (str ",\"network\":\"" network "\"") "")
|
||||
vcpu-json (if vcpu (str ",\"vcpu\":" vcpu) "")
|
||||
json (str "{\"shell\":\"" sh "\"" network-json vcpu-json "}")]
|
||||
input-files-json (build-input-files-json input-files)
|
||||
json (str "{\"shell\":\"" sh "\"" network-json vcpu-json input-files-json "}")]
|
||||
(println (str yellow "Session created (WebSocket required)" reset))
|
||||
(println (curl-post api-key "/sessions" json))))))
|
||||
|
||||
(defn service-command [action sid name ports bootstrap service-type network vcpu]
|
||||
(defn service-command [action sid name ports bootstrap bootstrap-file service-type network vcpu input-files]
|
||||
(let [api-key (get-api-key)]
|
||||
(case action
|
||||
:list (println (curl-get api-key "/services"))
|
||||
|
|
@ -292,10 +308,14 @@
|
|||
:create (when name
|
||||
(let [ports-json (if ports (str ",\"ports\":[" ports "]") "")
|
||||
bootstrap-json (if bootstrap (str ",\"bootstrap\":\"" (escape-json bootstrap) "\"") "")
|
||||
bootstrap-content-json (if bootstrap-file
|
||||
(str ",\"bootstrap_content\":\"" (escape-json (slurp bootstrap-file)) "\"")
|
||||
"")
|
||||
service-type-json (if service-type (str ",\"service_type\":\"" service-type "\"") "")
|
||||
network-json (if network (str ",\"network\":\"" network "\"") "")
|
||||
vcpu-json (if vcpu (str ",\"vcpu\":" vcpu) "")
|
||||
json (str "{\"name\":\"" name "\"" ports-json bootstrap-json service-type-json network-json vcpu-json "}")]
|
||||
input-files-json (build-input-files-json input-files)
|
||||
json (str "{\"name\":\"" name "\"" ports-json bootstrap-json bootstrap-content-json service-type-json network-json vcpu-json input-files-json "}")]
|
||||
(println (str green "Service created" reset))
|
||||
(println (curl-post api-key "/services" json)))))))
|
||||
|
||||
|
|
@ -359,19 +379,22 @@
|
|||
session-action nil
|
||||
session-id nil
|
||||
session-shell nil
|
||||
session-input-files []
|
||||
service-action nil
|
||||
service-id nil
|
||||
service-name nil
|
||||
service-ports nil
|
||||
service-bootstrap nil
|
||||
service-bootstrap-file nil
|
||||
service-type nil
|
||||
service-input-files []
|
||||
key-extend false
|
||||
mode :execute]
|
||||
(cond
|
||||
(empty? args)
|
||||
(case mode
|
||||
:session (session-command (or session-action :create) session-id session-shell network vcpu)
|
||||
:service (service-command (or service-action :create) service-id service-name service-ports service-bootstrap service-type network vcpu)
|
||||
:session (session-command (or session-action :create) session-id session-shell network vcpu session-input-files)
|
||||
:service (service-command (or service-action :create) service-id service-name service-ports service-bootstrap service-bootstrap-file service-type network vcpu service-input-files)
|
||||
:key (key-command key-extend)
|
||||
:execute (if file
|
||||
(execute-command file env-vars artifacts out-dir network vcpu)
|
||||
|
|
@ -382,117 +405,129 @@
|
|||
(System/exit 1))))
|
||||
|
||||
(= (first args) "session")
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend :session)
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend :session)
|
||||
|
||||
(= (first args) "service")
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend :service)
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend :service)
|
||||
|
||||
(= (first args) "key")
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend :key)
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend :key)
|
||||
|
||||
;; Key options
|
||||
(and (= mode :key) (= (first args) "--extend"))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type true mode)
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files true mode)
|
||||
|
||||
;; Session options
|
||||
(and (= mode :session) (= (first args) "--list"))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu :list session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu :list session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :session) (= (first args) "--kill"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu :kill (second args) session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu :kill (second args) session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :session) (or (= (first args) "--shell") (= (first args) "-s")))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id (second args)
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id (second args) session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :session) (= (first args) "-f"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell (conj session-input-files (second args))
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
;; Service options
|
||||
(and (= mode :service) (= (first args) "--list"))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:list service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:list service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--info"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:info (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:info (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--logs"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:logs (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:logs (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--freeze"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:sleep (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:sleep (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--unfreeze"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:wake (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:wake (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--destroy"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:destroy (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:destroy (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--execute"))
|
||||
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:execute (second args) service-name service-ports (nth args 2) service-type key-extend mode)
|
||||
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:execute (second args) service-name service-ports (nth args 2) service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--dump-bootstrap") (>= (count args) 3) (not (.startsWith (nth args 2) "-")))
|
||||
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:dump-bootstrap (second args) service-name service-ports service-bootstrap (nth args 2) key-extend mode)
|
||||
(recur (rest (rest (rest args))) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:dump-bootstrap (second args) service-name service-ports service-bootstrap (nth args 2) service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--dump-bootstrap"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:dump-bootstrap (second args) service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:dump-bootstrap (second args) service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--name"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
:create service-id (second args) service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
:create service-id (second args) service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--ports"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name (second args) service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name (second args) service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--bootstrap"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports (second args) service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports (second args) service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--bootstrap-file"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap (second args) service-type service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "--type"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap (second args) key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file (second args) service-input-files key-extend mode)
|
||||
|
||||
(and (= mode :service) (= (first args) "-f"))
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type (conj service-input-files (second args)) key-extend mode)
|
||||
|
||||
;; Execute options
|
||||
(= (first args) "-e")
|
||||
(let [[k v] (str/split (second args) #"=" 2)]
|
||||
(recur (rest (rest args)) file (conj env-vars [k v]) artifacts out-dir network vcpu
|
||||
session-action session-id session-shell service-action service-id service-name service-ports service-bootstrap service-type key-extend mode))
|
||||
session-action session-id session-shell session-input-files service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode))
|
||||
|
||||
(= (first args) "-a")
|
||||
(recur (rest args) file env-vars true out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest args) file env-vars true out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(= (first args) "-o")
|
||||
(recur (rest (rest args)) file env-vars artifacts (second args) network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts (second args) network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(= (first args) "-n")
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir (second args) vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir (second args) vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
(= (first args) "-v")
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network (Integer/parseInt (second args)) session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest (rest args)) file env-vars artifacts out-dir network (Integer/parseInt (second args)) session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
;; Source file
|
||||
(and (= mode :execute) (not (.startsWith (first args) "-")) (nil? file))
|
||||
(recur (rest args) (first args) env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode)
|
||||
(recur (rest args) (first args) env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode)
|
||||
|
||||
:else
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell
|
||||
service-action service-id service-name service-ports service-bootstrap service-type key-extend mode))))
|
||||
(recur (rest args) file env-vars artifacts out-dir network vcpu session-action session-id session-shell session-input-files
|
||||
service-action service-id service-name service-ports service-bootstrap service-bootstrap-file service-type service-input-files key-extend mode))))
|
||||
|
||||
(parse-args *command-line-args*)
|
||||
|
|
|
|||
114
un.cob
114
un.cob
|
|
@ -75,6 +75,8 @@
|
|||
01 WS-DOMAINS PIC X(256).
|
||||
01 WS-SERVICE-TYPE PIC X(64).
|
||||
01 WS-BOOTSTRAP PIC X(2048).
|
||||
01 WS-BOOTSTRAP-FILE PIC X(256).
|
||||
01 WS-INPUT-FILES PIC X(1024).
|
||||
01 WS-PORTAL-BASE PIC X(256) VALUE
|
||||
"https://unsandbox.com".
|
||||
01 WS-EXTEND-FLAG PIC X(8).
|
||||
|
|
@ -155,6 +157,9 @@
|
|||
STOP RUN
|
||||
END-IF.
|
||||
|
||||
* Initialize session parameters
|
||||
MOVE SPACES TO WS-INPUT-FILES.
|
||||
|
||||
* Parse session arguments (simplified)
|
||||
* For full implementation, would need to parse multiple args
|
||||
ACCEPT WS-ARG2 FROM ARGUMENT-VALUE.
|
||||
|
|
@ -166,9 +171,8 @@
|
|||
ACCEPT WS-ID FROM ARGUMENT-VALUE
|
||||
PERFORM SESSION-KILL
|
||||
ELSE
|
||||
DISPLAY "Error: Use --list or --kill ID"
|
||||
UPON SYSERR
|
||||
MOVE 1 TO RETURN-CODE
|
||||
PERFORM PARSE-SESSION-CREATE-ARGS
|
||||
PERFORM SESSION-CREATE
|
||||
END-IF
|
||||
END-IF.
|
||||
|
||||
|
|
@ -187,6 +191,8 @@
|
|||
MOVE SPACES TO WS-DOMAINS.
|
||||
MOVE SPACES TO WS-SERVICE-TYPE.
|
||||
MOVE SPACES TO WS-BOOTSTRAP.
|
||||
MOVE SPACES TO WS-BOOTSTRAP-FILE.
|
||||
MOVE SPACES TO WS-INPUT-FILES.
|
||||
|
||||
* Parse service arguments
|
||||
ACCEPT WS-ARG2 FROM ARGUMENT-VALUE.
|
||||
|
|
@ -356,6 +362,59 @@
|
|||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
PARSE-SESSION-CREATE-ARGS.
|
||||
* Parse arguments for session creation
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE.
|
||||
PERFORM UNTIL WS-ARG3 = SPACES
|
||||
IF WS-ARG3 = "-f"
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE
|
||||
IF WS-INPUT-FILES NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-INPUT-FILES) ","
|
||||
FUNCTION TRIM(WS-ARG3)
|
||||
DELIMITED BY SIZE INTO WS-INPUT-FILES
|
||||
END-STRING
|
||||
ELSE
|
||||
MOVE WS-ARG3 TO WS-INPUT-FILES
|
||||
END-IF
|
||||
END-IF
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE
|
||||
END-PERFORM.
|
||||
|
||||
SESSION-CREATE.
|
||||
* Build curl command for session creation with input_files support
|
||||
STRING "INPUT_FILES=''; "
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
IF WS-INPUT-FILES NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
"IFS=',' read -ra FILES <<< '"
|
||||
FUNCTION TRIM(WS-INPUT-FILES)
|
||||
"'; "
|
||||
"for f in \"${FILES[@]}\"; do "
|
||||
"b64=$(base64 -w0 \"$f\" 2>/dev/null || base64 \"$f\"); "
|
||||
"name=$(basename \"$f\"); "
|
||||
"if [ -n \"$INPUT_FILES\" ]; then INPUT_FILES=\"$INPUT_FILES,\"; fi; "
|
||||
"INPUT_FILES=\"$INPUT_FILES{\\\"filename\\\":\\\"$name\\\",\\\"content\\\":\\\"$b64\\\"}\"; "
|
||||
"done; "
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
END-IF.
|
||||
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
"if [ -n \"$INPUT_FILES\" ]; then "
|
||||
"JSON='{\"shell\":\"bash\",\"input_files\":['\"$INPUT_FILES\"']}'; "
|
||||
"else JSON='{\"shell\":\"bash\"}'; fi; "
|
||||
"curl -s -X POST https://api.unsandbox.com/sessions "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY)
|
||||
"' -d \"$JSON\" && "
|
||||
"echo -e '\x1b[33mSession created (WebSocket required)\x1b[0m'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING.
|
||||
|
||||
CALL "SYSTEM" USING WS-CURL-CMD.
|
||||
|
||||
SERVICE-LIST.
|
||||
STRING "curl -s -X GET https://api.unsandbox.com/services "
|
||||
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY)
|
||||
|
|
@ -487,6 +546,18 @@
|
|||
ACCEPT WS-SERVICE-TYPE FROM ARGUMENT-VALUE
|
||||
ELSE IF WS-ARG3 = "--bootstrap"
|
||||
ACCEPT WS-BOOTSTRAP FROM ARGUMENT-VALUE
|
||||
ELSE IF WS-ARG3 = "--bootstrap-file"
|
||||
ACCEPT WS-BOOTSTRAP-FILE FROM ARGUMENT-VALUE
|
||||
ELSE IF WS-ARG3 = "-f"
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE
|
||||
IF WS-INPUT-FILES NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-INPUT-FILES) ","
|
||||
FUNCTION TRIM(WS-ARG3)
|
||||
DELIMITED BY SIZE INTO WS-INPUT-FILES
|
||||
END-STRING
|
||||
ELSE
|
||||
MOVE WS-ARG3 TO WS-INPUT-FILES
|
||||
END-IF
|
||||
END-IF
|
||||
ACCEPT WS-ARG3 FROM ARGUMENT-VALUE
|
||||
END-PERFORM.
|
||||
|
|
@ -542,6 +613,43 @@
|
|||
END-STRING
|
||||
END-IF.
|
||||
|
||||
* Add bootstrap_content from file if provided
|
||||
IF WS-BOOTSTRAP-FILE NOT = SPACES
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
"' | jq --rawfile b '"
|
||||
FUNCTION TRIM(WS-BOOTSTRAP-FILE)
|
||||
"' '. + {bootstrap_content: $b}' | tr -d '\\n' | curl "
|
||||
"-s -X POST https://api.unsandbox.com/services "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY)
|
||||
"' -d @- | jq -r '.id + "" created""'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
CALL "SYSTEM" USING WS-CURL-CMD
|
||||
EXIT PARAGRAPH
|
||||
END-IF.
|
||||
|
||||
* Add input_files if provided (use shell script for base64)
|
||||
IF WS-INPUT-FILES NOT = SPACES
|
||||
STRING "INPUT_FILES=''; "
|
||||
"IFS=',' read -ra FILES <<< '"
|
||||
FUNCTION TRIM(WS-INPUT-FILES)
|
||||
"'; "
|
||||
"for f in \"${FILES[@]}\"; do "
|
||||
"b64=$(base64 -w0 \"$f\" 2>/dev/null || base64 \"$f\"); "
|
||||
"name=$(basename \"$f\"); "
|
||||
"if [ -n \"$INPUT_FILES\" ]; then INPUT_FILES=\"$INPUT_FILES,\"; fi; "
|
||||
"INPUT_FILES=\"$INPUT_FILES{\\\"filename\\\":\\\"$name\\\",\\\"content\\\":\\\"$b64\\\"}\"; "
|
||||
"done; "
|
||||
FUNCTION TRIM(WS-CURL-CMD)
|
||||
",\"input_files\":['\"$INPUT_FILES\"']}' | "
|
||||
"jq -r '.id + "" created""'"
|
||||
DELIMITED BY SIZE INTO WS-CURL-CMD
|
||||
END-STRING
|
||||
CALL "SYSTEM" USING WS-CURL-CMD
|
||||
EXIT PARAGRAPH
|
||||
END-IF.
|
||||
|
||||
* Close JSON and add output formatting
|
||||
STRING FUNCTION TRIM(WS-CURL-CMD)
|
||||
"}' | jq -r '.id + "" created""'"
|
||||
|
|
|
|||
84
un.cpp
84
un.cpp
|
|
@ -100,6 +100,25 @@ string escape_json(const string& s) {
|
|||
return o.str();
|
||||
}
|
||||
|
||||
// Base64 encoding
|
||||
static const char b64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
string base64_encode(const string& input) {
|
||||
string output;
|
||||
int val = 0, valb = -6;
|
||||
for (unsigned char c : input) {
|
||||
val = (val << 8) + c;
|
||||
valb += 8;
|
||||
while (valb >= 0) {
|
||||
output.push_back(b64_table[(val >> valb) & 0x3F]);
|
||||
valb -= 6;
|
||||
}
|
||||
}
|
||||
if (valb > -6) output.push_back(b64_table[((val << 8) >> (valb + 8)) & 0x3F]);
|
||||
while (output.size() % 4) output.push_back('=');
|
||||
return output;
|
||||
}
|
||||
|
||||
string exec_curl(const string& cmd) {
|
||||
FILE* pipe = popen(cmd.c_str(), "r");
|
||||
if (!pipe) return "";
|
||||
|
|
@ -233,7 +252,7 @@ void cmd_execute(const string& source_file, const vector<string>& envs, const ve
|
|||
exit(exit_code);
|
||||
}
|
||||
|
||||
void cmd_session(bool list, const string& kill, const string& shell, const string& network, int vcpu, bool tmux, bool screen, const string& public_key, const string& secret_key) {
|
||||
void cmd_session(bool list, const string& kill, const string& shell, const string& network, int vcpu, bool tmux, bool screen, const vector<string>& files, const string& public_key, const string& secret_key) {
|
||||
if (list) {
|
||||
string auth_headers = build_auth_headers("GET", "/sessions", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/sessions' " + auth_headers;
|
||||
|
|
@ -255,6 +274,26 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
|
|||
if (vcpu > 0) json << ",\"vcpu\":" << vcpu;
|
||||
if (tmux) json << ",\"persistence\":\"tmux\"";
|
||||
if (screen) json << ",\"persistence\":\"screen\"";
|
||||
|
||||
// Input files
|
||||
if (!files.empty()) {
|
||||
json << ",\"input_files\":[";
|
||||
for (size_t i = 0; i < files.size(); i++) {
|
||||
if (i > 0) json << ",";
|
||||
ifstream file(files[i], ios::binary);
|
||||
if (!file) {
|
||||
cerr << RED << "Error: Input file not found: " << files[i] << RESET << endl;
|
||||
exit(1);
|
||||
}
|
||||
ostringstream content;
|
||||
content << file.rdbuf();
|
||||
string b64 = base64_encode(content.str());
|
||||
string filename = files[i].substr(files[i].find_last_of("/\\") + 1);
|
||||
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
|
||||
}
|
||||
json << "]";
|
||||
}
|
||||
|
||||
json << "}";
|
||||
|
||||
cout << YELLOW << "Creating session..." << RESET << endl;
|
||||
|
|
@ -266,7 +305,7 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
|
|||
cout << exec_curl(cmd) << endl;
|
||||
}
|
||||
|
||||
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const string& public_key, const string& secret_key) {
|
||||
void cmd_service(const string& name, const string& ports, const string& type, const string& bootstrap, const string& bootstrap_file, const vector<string>& files, bool list, const string& info, const string& logs, const string& tail, const string& sleep, const string& wake, const string& destroy, const string& execute, const string& command, const string& dump_bootstrap, const string& dump_file, const string& network, int vcpu, const string& public_key, const string& secret_key) {
|
||||
if (list) {
|
||||
string auth_headers = build_auth_headers("GET", "/services", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services' " + auth_headers;
|
||||
|
|
@ -415,14 +454,36 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
if (!ports.empty()) json << ",\"ports\":[" << ports << "]";
|
||||
if (!type.empty()) json << ",\"service_type\":\"" << type << "\"";
|
||||
if (!bootstrap.empty()) {
|
||||
json << ",\"bootstrap\":\"" << escape_json(bootstrap) << "\"";
|
||||
}
|
||||
if (!bootstrap_file.empty()) {
|
||||
struct stat st;
|
||||
if (stat(bootstrap.c_str(), &st) == 0) {
|
||||
string boot_code = read_file(bootstrap);
|
||||
json << ",\"bootstrap\":\"" << escape_json(boot_code) << "\"";
|
||||
if (stat(bootstrap_file.c_str(), &st) == 0) {
|
||||
string boot_code = read_file(bootstrap_file);
|
||||
json << ",\"bootstrap_content\":\"" << escape_json(boot_code) << "\"";
|
||||
} else {
|
||||
json << ",\"bootstrap\":\"" << escape_json(bootstrap) << "\"";
|
||||
cerr << RED << "Error: Bootstrap file not found: " << bootstrap_file << RESET << endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
// Input files
|
||||
if (!files.empty()) {
|
||||
json << ",\"input_files\":[";
|
||||
for (size_t i = 0; i < files.size(); i++) {
|
||||
if (i > 0) json << ",";
|
||||
ifstream file(files[i], ios::binary);
|
||||
if (!file) {
|
||||
cerr << RED << "Error: Input file not found: " << files[i] << RESET << endl;
|
||||
exit(1);
|
||||
}
|
||||
ostringstream content;
|
||||
content << file.rdbuf();
|
||||
string b64 = base64_encode(content.str());
|
||||
string filename = files[i].substr(files[i].find_last_of("/\\") + 1);
|
||||
json << "{\"filename\":\"" << filename << "\",\"content_base64\":\"" << b64 << "\"}";
|
||||
}
|
||||
json << "]";
|
||||
}
|
||||
if (!network.empty()) json << ",\"network\":\"" << network << "\"";
|
||||
if (vcpu > 0) json << ",\"vcpu\":" << vcpu;
|
||||
json << "}";
|
||||
|
|
@ -547,12 +608,14 @@ int main(int argc, char* argv[]) {
|
|||
string kill, shell, network;
|
||||
int vcpu = 0;
|
||||
bool tmux = false, screen = false;
|
||||
vector<string> files;
|
||||
|
||||
for (int i = 2; i < argc; i++) {
|
||||
string arg = argv[i];
|
||||
if (arg == "--list") list = true;
|
||||
else if (arg == "--kill" && i+1 < argc) kill = argv[++i];
|
||||
else if (arg == "--shell" && i+1 < argc) shell = argv[++i];
|
||||
else if (arg == "-f" && i+1 < argc) files.push_back(argv[++i]);
|
||||
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
||||
else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
|
||||
else if (arg == "--tmux") tmux = true;
|
||||
|
|
@ -560,15 +623,16 @@ int main(int argc, char* argv[]) {
|
|||
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, public_key, secret_key);
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, files, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cmd_type == "service") {
|
||||
string name, ports, type, bootstrap;
|
||||
string name, ports, type, bootstrap, bootstrap_file;
|
||||
bool list = false;
|
||||
string info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network;
|
||||
int vcpu = 0;
|
||||
vector<string> files;
|
||||
|
||||
for (int i = 2; i < argc; i++) {
|
||||
string arg = argv[i];
|
||||
|
|
@ -576,6 +640,8 @@ int main(int argc, char* argv[]) {
|
|||
else if (arg == "--ports" && i+1 < argc) ports = argv[++i];
|
||||
else if (arg == "--type" && i+1 < argc) type = argv[++i];
|
||||
else if (arg == "--bootstrap" && i+1 < argc) bootstrap = argv[++i];
|
||||
else if (arg == "--bootstrap-file" && i+1 < argc) bootstrap_file = argv[++i];
|
||||
else if (arg == "-f" && i+1 < argc) files.push_back(argv[++i]);
|
||||
else if (arg == "--list") list = true;
|
||||
else if (arg == "--info" && i+1 < argc) info = argv[++i];
|
||||
else if (arg == "--logs" && i+1 < argc) logs = argv[++i];
|
||||
|
|
@ -592,7 +658,7 @@ int main(int argc, char* argv[]) {
|
|||
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_service(name, ports, type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, public_key, secret_key);
|
||||
cmd_service(name, ports, type, bootstrap, bootstrap_file, files, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network, vcpu, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
68
un.cr
68
un.cr
|
|
@ -262,8 +262,36 @@ def cmd_session(args)
|
|||
return
|
||||
end
|
||||
|
||||
STDERR.puts "#{RED}Error: Use --list or --kill#{RESET}"
|
||||
exit 1
|
||||
# Create new session
|
||||
payload = JSON.parse({shell: "bash"}.to_json)
|
||||
|
||||
if network = args[:network]?.as?(String)
|
||||
payload.as_h["network"] = JSON::Any.new(network)
|
||||
end
|
||||
|
||||
# Add input files
|
||||
if files = args[:files]?.as?(Array(String))
|
||||
input_files = [] of JSON::Any
|
||||
files.each do |filepath|
|
||||
unless File.exists?(filepath)
|
||||
STDERR.puts "#{RED}Error: Input file not found: #{filepath}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
content = Base64.strict_encode(File.read(filepath))
|
||||
input_files << JSON.parse({
|
||||
filename: File.basename(filepath),
|
||||
content_base64: content
|
||||
}.to_json)
|
||||
end
|
||||
unless input_files.empty?
|
||||
payload.as_h["input_files"] = JSON.parse(input_files.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
puts "#{YELLOW}Creating session...#{RESET}"
|
||||
result = api_request("/sessions", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Session created: #{result["id"]?.try(&.as_s?) || "N/A"}#{RESET}"
|
||||
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
||||
end
|
||||
|
||||
def cmd_key(args)
|
||||
|
|
@ -469,11 +497,16 @@ def cmd_service(args)
|
|||
|
||||
# Add bootstrap
|
||||
if bootstrap = args[:bootstrap]?.as?(String)
|
||||
# Check if bootstrap is a file
|
||||
if File.exists?(bootstrap)
|
||||
payload.as_h["bootstrap"] = JSON::Any.new(File.read(bootstrap))
|
||||
payload.as_h["bootstrap"] = JSON::Any.new(bootstrap)
|
||||
end
|
||||
|
||||
# Add bootstrap_file
|
||||
if bootstrap_file = args[:bootstrap_file]?.as?(String)
|
||||
if File.exists?(bootstrap_file)
|
||||
payload.as_h["bootstrap_content"] = JSON::Any.new(File.read(bootstrap_file))
|
||||
else
|
||||
payload.as_h["bootstrap"] = JSON::Any.new(bootstrap)
|
||||
STDERR.puts "#{RED}Error: Bootstrap file not found: #{bootstrap_file}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -482,6 +515,25 @@ def cmd_service(args)
|
|||
payload.as_h["network"] = JSON::Any.new(network)
|
||||
end
|
||||
|
||||
# Add input files
|
||||
if files = args[:files]?.as?(Array(String))
|
||||
input_files = [] of JSON::Any
|
||||
files.each do |filepath|
|
||||
unless File.exists?(filepath)
|
||||
STDERR.puts "#{RED}Error: Input file not found: #{filepath}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
content = Base64.strict_encode(File.read(filepath))
|
||||
input_files << JSON.parse({
|
||||
filename: File.basename(filepath),
|
||||
content_base64: content
|
||||
}.to_json)
|
||||
end
|
||||
unless input_files.empty?
|
||||
payload.as_h["input_files"] = JSON.parse(input_files.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
# Create service
|
||||
result = api_request("/services", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Service created: #{result["id"]?.try(&.as_s?) || "N/A"}#{RESET}"
|
||||
|
|
@ -521,6 +573,7 @@ def main
|
|||
domains: nil,
|
||||
service_type: nil,
|
||||
bootstrap: nil,
|
||||
bootstrap_file: nil,
|
||||
extend: false
|
||||
} of Symbol => (String | Array(String) | Bool | Nil)
|
||||
|
||||
|
|
@ -548,7 +601,8 @@ def main
|
|||
opts.on("--ports=PORTS", "Comma-separated ports") { |p| args[:ports] = p }
|
||||
opts.on("--domains=DOMAINS", "Comma-separated domains") { |d| args[:domains] = d }
|
||||
opts.on("--type=TYPE", "Service type for SRV records") { |t| args[:service_type] = t }
|
||||
opts.on("--bootstrap=CMD", "Bootstrap command/file") { |b| args[:bootstrap] = b }
|
||||
opts.on("--bootstrap=CMD", "Bootstrap command or URI") { |b| args[:bootstrap] = b }
|
||||
opts.on("--bootstrap-file=FILE", "Upload local file as bootstrap script") { |f| args[:bootstrap_file] = f }
|
||||
opts.on("--extend", "Open browser to extend/renew key") { args[:extend] = true }
|
||||
|
||||
opts.unknown_args do |before, after|
|
||||
|
|
|
|||
54
un.d
54
un.d
|
|
@ -88,6 +88,31 @@ string escapeJson(string s) {
|
|||
return result;
|
||||
}
|
||||
|
||||
string readAndBase64(string filepath) {
|
||||
import std.base64 : Base64;
|
||||
try {
|
||||
auto content = readText(filepath);
|
||||
return Base64.encode(cast(ubyte[])content);
|
||||
} catch (Exception e) {
|
||||
stderr.writefln("%sError: Cannot read file: %s%s", RED, filepath, RESET);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
string buildInputFilesJson(string[] files) {
|
||||
if (files.length == 0) return "";
|
||||
string[] fileJsons;
|
||||
foreach (f; files) {
|
||||
string b64 = readAndBase64(f);
|
||||
if (b64.empty) continue;
|
||||
string basename = baseName(f);
|
||||
fileJsons ~= format(`{"filename":"%s","content":"%s"}`, escapeJson(basename), b64);
|
||||
}
|
||||
if (fileJsons.length == 0) return "";
|
||||
import std.array : join;
|
||||
return format(`,"input_files":[%s]`, fileJsons.join(","));
|
||||
}
|
||||
|
||||
string computeHmac(string secretKey, string message) {
|
||||
import std.process : pipeShell, Redirect, wait;
|
||||
import std.stdio : File;
|
||||
|
|
@ -173,7 +198,7 @@ void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network
|
|||
writeln(result);
|
||||
}
|
||||
|
||||
void cmdSession(bool list, string kill, string shell, string network, int vcpu, bool tmux, bool screen, string publicKey, string secretKey) {
|
||||
void cmdSession(bool list, string kill, string shell, string network, int vcpu, bool tmux, bool screen, string[] inputFiles, string publicKey, string secretKey) {
|
||||
if (list) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/sessions' %s`, API_BASE, authHeaders);
|
||||
|
|
@ -195,6 +220,7 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
|
|||
if (vcpu > 0) json ~= format(`,"vcpu":%d`, vcpu);
|
||||
if (tmux) json ~= `,"persistence":"tmux"`;
|
||||
if (screen) json ~= `,"persistence":"screen"`;
|
||||
json ~= buildInputFilesJson(inputFiles);
|
||||
json ~= "}";
|
||||
|
||||
writefln("%sCreating session...%s", YELLOW, RESET);
|
||||
|
|
@ -203,7 +229,7 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
|
|||
writeln(execCurl(cmd));
|
||||
}
|
||||
|
||||
void cmdService(string name, string ports, string bootstrap, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string publicKey, string secretKey) {
|
||||
void cmdService(string name, string ports, string bootstrap, string bootstrapFile, string type, bool list, string info, string logs, string tail, string sleep, string wake, string destroy, string execute, string command, string dumpBootstrap, string dumpFile, string network, int vcpu, string[] inputFiles, string publicKey, string secretKey) {
|
||||
if (list) {
|
||||
string authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/services' %s`, API_BASE, authHeaders);
|
||||
|
|
@ -340,15 +366,20 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
|
|||
if (!ports.empty) json ~= format(`,"ports":[%s]`, ports);
|
||||
if (!type.empty) json ~= format(`,"service_type":"%s"`, type);
|
||||
if (!bootstrap.empty) {
|
||||
if (exists(bootstrap)) {
|
||||
string bootCode = readText(bootstrap);
|
||||
json ~= format(`,"bootstrap":"%s"`, escapeJson(bootCode));
|
||||
json ~= format(`,"bootstrap":"%s"`, escapeJson(bootstrap));
|
||||
}
|
||||
if (!bootstrapFile.empty) {
|
||||
if (exists(bootstrapFile)) {
|
||||
string bootCode = readText(bootstrapFile);
|
||||
json ~= format(`,"bootstrap_content":"%s"`, escapeJson(bootCode));
|
||||
} else {
|
||||
json ~= format(`,"bootstrap":"%s"`, escapeJson(bootstrap));
|
||||
stderr.writefln("%sError: Bootstrap file not found: %s%s", RED, bootstrapFile, RESET);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if (!network.empty) json ~= format(`,"network":"%s"`, network);
|
||||
if (vcpu > 0) json ~= format(`,"vcpu":%d`, vcpu);
|
||||
json ~= buildInputFilesJson(inputFiles);
|
||||
json ~= "}";
|
||||
|
||||
writefln("%sCreating service...%s", YELLOW, RESET);
|
||||
|
|
@ -505,6 +536,7 @@ int main(string[] args) {
|
|||
string kill, shell, network;
|
||||
int vcpu = 0;
|
||||
bool tmux = false, screen = false;
|
||||
string[] inputFiles;
|
||||
|
||||
for (size_t i = 2; i < args.length; i++) {
|
||||
if (args[i] == "--list") list = true;
|
||||
|
|
@ -514,23 +546,26 @@ int main(string[] args) {
|
|||
else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]);
|
||||
else if (args[i] == "--tmux") tmux = true;
|
||||
else if (args[i] == "--screen") screen = true;
|
||||
else if (args[i] == "-f" && i+1 < args.length) inputFiles ~= args[++i];
|
||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
|
||||
}
|
||||
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey);
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, inputFiles, publicKey, secretKey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args[1] == "service") {
|
||||
string name, ports, bootstrap, type;
|
||||
string name, ports, bootstrap, bootstrapFile, type;
|
||||
bool list = false;
|
||||
string info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network;
|
||||
int vcpu = 0;
|
||||
string[] inputFiles;
|
||||
|
||||
for (size_t i = 2; i < args.length; i++) {
|
||||
if (args[i] == "--name" && i+1 < args.length) name = args[++i];
|
||||
else if (args[i] == "--ports" && i+1 < args.length) ports = args[++i];
|
||||
else if (args[i] == "--bootstrap" && i+1 < args.length) bootstrap = args[++i];
|
||||
else if (args[i] == "--bootstrap-file" && i+1 < args.length) bootstrapFile = args[++i];
|
||||
else if (args[i] == "--type" && i+1 < args.length) type = args[++i];
|
||||
else if (args[i] == "--list") list = true;
|
||||
else if (args[i] == "--info" && i+1 < args.length) info = args[++i];
|
||||
|
|
@ -545,10 +580,11 @@ int main(string[] args) {
|
|||
else if (args[i] == "--dump-file" && i+1 < args.length) dumpFile = args[++i];
|
||||
else if (args[i] == "-n" && i+1 < args.length) network = args[++i];
|
||||
else if (args[i] == "-v" && i+1 < args.length) vcpu = to!int(args[++i]);
|
||||
else if (args[i] == "-f" && i+1 < args.length) inputFiles ~= args[++i];
|
||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
|
||||
}
|
||||
|
||||
cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey);
|
||||
cmdService(name, ports, bootstrap, bootstrapFile, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, inputFiles, publicKey, secretKey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
49
un.dart
49
un.dart
|
|
@ -86,6 +86,7 @@ class Args {
|
|||
String? servicePorts;
|
||||
String? serviceType;
|
||||
String? serviceBootstrap;
|
||||
String? serviceBootstrapFile;
|
||||
String? serviceInfo;
|
||||
String? serviceLogs;
|
||||
String? serviceTail;
|
||||
|
|
@ -305,6 +306,24 @@ Future<void> cmdSession(Args args) async {
|
|||
payload['vcpu'] = args.vcpu;
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if (args.files.isNotEmpty) {
|
||||
final inputFiles = <Map<String, String>>[];
|
||||
for (final filepath in args.files) {
|
||||
final file = File(filepath);
|
||||
if (!await file.exists()) {
|
||||
stderr.writeln('${red}Error: Input file not found: $filepath$reset');
|
||||
exit(1);
|
||||
}
|
||||
final content = await file.readAsBytes();
|
||||
inputFiles.add({
|
||||
'filename': filepath.split('/').last,
|
||||
'content_base64': base64Encode(content),
|
||||
});
|
||||
}
|
||||
payload['input_files'] = inputFiles;
|
||||
}
|
||||
|
||||
print('${yellow}Creating session...$reset');
|
||||
final result = await apiRequestCurl('/sessions', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Session created: ${result['id'] ?? 'N/A'}$reset');
|
||||
|
|
@ -426,6 +445,15 @@ Future<void> cmdService(Args args) async {
|
|||
if (args.serviceBootstrap != null) {
|
||||
payload['bootstrap'] = args.serviceBootstrap;
|
||||
}
|
||||
if (args.serviceBootstrapFile != null) {
|
||||
final file = File(args.serviceBootstrapFile!);
|
||||
if (await file.exists()) {
|
||||
payload['bootstrap_content'] = await file.readAsString();
|
||||
} else {
|
||||
stderr.writeln('${red}Error: Bootstrap file not found: ${args.serviceBootstrapFile}$reset');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if (args.network != null) {
|
||||
payload['network'] = args.network;
|
||||
}
|
||||
|
|
@ -433,6 +461,24 @@ Future<void> cmdService(Args args) async {
|
|||
payload['vcpu'] = args.vcpu;
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if (args.files.isNotEmpty) {
|
||||
final inputFiles = <Map<String, String>>[];
|
||||
for (final filepath in args.files) {
|
||||
final file = File(filepath);
|
||||
if (!await file.exists()) {
|
||||
stderr.writeln('${red}Error: Input file not found: $filepath$reset');
|
||||
exit(1);
|
||||
}
|
||||
final content = await file.readAsBytes();
|
||||
inputFiles.add({
|
||||
'filename': filepath.split('/').last,
|
||||
'content_base64': base64Encode(content),
|
||||
});
|
||||
}
|
||||
payload['input_files'] = inputFiles;
|
||||
}
|
||||
|
||||
final result = await apiRequestCurl('/services', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Service created: ${result['id'] ?? 'N/A'}$reset');
|
||||
print('Name: ${result['name'] ?? 'N/A'}');
|
||||
|
|
@ -573,6 +619,9 @@ Args parseArgs(List<String> argv) {
|
|||
case '--bootstrap':
|
||||
args.serviceBootstrap = argv[++i];
|
||||
break;
|
||||
case '--bootstrap-file':
|
||||
args.serviceBootstrapFile = argv[++i];
|
||||
break;
|
||||
case '--info':
|
||||
args.serviceInfo = argv[++i];
|
||||
break;
|
||||
|
|
|
|||
48
un.erl
48
un.erl
|
|
@ -105,7 +105,9 @@ session_command(["--kill", SessionId | _]) ->
|
|||
session_command(Args) ->
|
||||
ApiKey = get_api_key(),
|
||||
Shell = get_shell_opt(Args, "bash"),
|
||||
Json = "{\"shell\":\"" ++ Shell ++ "\"}",
|
||||
InputFiles = get_input_files(Args),
|
||||
InputFilesJson = build_input_files_json(InputFiles),
|
||||
Json = "{\"shell\":\"" ++ Shell ++ "\"" ++ InputFilesJson ++ "}",
|
||||
TmpFile = write_temp_file(Json),
|
||||
Response = curl_post(ApiKey, "/sessions", TmpFile),
|
||||
file:delete(TmpFile),
|
||||
|
|
@ -199,7 +201,9 @@ service_command(Args) ->
|
|||
ApiKey = get_api_key(),
|
||||
Ports = get_service_ports(Args),
|
||||
Bootstrap = get_service_bootstrap(Args),
|
||||
BootstrapFile = get_service_bootstrap_file(Args),
|
||||
Type = get_service_type(Args),
|
||||
InputFiles = get_input_files(Args),
|
||||
PortsJson = case Ports of
|
||||
undefined -> "";
|
||||
P -> ",\"ports\":[" ++ P ++ "]"
|
||||
|
|
@ -208,11 +212,24 @@ service_command(Args) ->
|
|||
undefined -> "";
|
||||
B -> ",\"bootstrap\":\"" ++ escape_json(B) ++ "\""
|
||||
end,
|
||||
BootstrapContentJson = case BootstrapFile of
|
||||
undefined -> "";
|
||||
BF ->
|
||||
case file:read_file(BF) of
|
||||
{ok, ContentBin} ->
|
||||
Content = binary_to_list(ContentBin),
|
||||
",\"bootstrap_content\":\"" ++ escape_json(Content) ++ "\"";
|
||||
{error, _} ->
|
||||
io:format(standard_error, "\033[31mError: Bootstrap file not found: ~s\033[0m~n", [BF]),
|
||||
halt(1)
|
||||
end
|
||||
end,
|
||||
TypeJson = case Type of
|
||||
undefined -> "";
|
||||
T -> ",\"service_type\":\"" ++ T ++ "\""
|
||||
end,
|
||||
Json = "{\"name\":\"" ++ Name ++ "\"" ++ PortsJson ++ BootstrapJson ++ TypeJson ++ "}",
|
||||
InputFilesJson = build_input_files_json(InputFiles),
|
||||
Json = "{\"name\":\"" ++ Name ++ "\"" ++ PortsJson ++ BootstrapJson ++ BootstrapContentJson ++ TypeJson ++ InputFilesJson ++ "}",
|
||||
TmpFile = write_temp_file(Json),
|
||||
Response = curl_post(ApiKey, "/services", TmpFile),
|
||||
file:delete(TmpFile),
|
||||
|
|
@ -409,6 +426,23 @@ escape_json([C | Rest], Acc) ->
|
|||
build_json(Language, Code) ->
|
||||
"{\"language\":\"" ++ Language ++ "\",\"code\":\"" ++ escape_json(Code) ++ "\"}".
|
||||
|
||||
read_and_base64(Filepath) ->
|
||||
case file:read_file(Filepath) of
|
||||
{ok, Content} ->
|
||||
base64:encode_to_string(Content);
|
||||
{error, _} ->
|
||||
""
|
||||
end.
|
||||
|
||||
build_input_files_json([]) -> "";
|
||||
build_input_files_json(Files) ->
|
||||
FileJsons = lists:map(fun(F) ->
|
||||
B64 = read_and_base64(F),
|
||||
Basename = filename:basename(F),
|
||||
"{\"filename\":\"" ++ escape_json(Basename) ++ "\",\"content\":\"" ++ B64 ++ "\"}"
|
||||
end, Files),
|
||||
",\"input_files\":[" ++ string:join(FileJsons, ",") ++ "]".
|
||||
|
||||
write_temp_file(Data) ->
|
||||
TmpFile = "/tmp/un_erl_" ++ integer_to_list(rand:uniform(999999)) ++ ".json",
|
||||
file:write_file(TmpFile, Data),
|
||||
|
|
@ -484,10 +518,20 @@ get_service_bootstrap([]) -> undefined;
|
|||
get_service_bootstrap(["--bootstrap", Bootstrap | _]) -> Bootstrap;
|
||||
get_service_bootstrap([_ | Rest]) -> get_service_bootstrap(Rest).
|
||||
|
||||
get_service_bootstrap_file([]) -> undefined;
|
||||
get_service_bootstrap_file(["--bootstrap-file", BootstrapFile | _]) -> BootstrapFile;
|
||||
get_service_bootstrap_file([_ | Rest]) -> get_service_bootstrap_file(Rest).
|
||||
|
||||
get_service_type([]) -> undefined;
|
||||
get_service_type(["--type", Type | _]) -> Type;
|
||||
get_service_type([_ | Rest]) -> get_service_type(Rest).
|
||||
|
||||
get_input_files(Args) -> get_input_files(Args, []).
|
||||
|
||||
get_input_files([], Acc) -> lists:reverse(Acc);
|
||||
get_input_files(["-f", File | Rest], Acc) -> get_input_files(Rest, [File | Acc]);
|
||||
get_input_files([_ | Rest], Acc) -> get_input_files(Rest, Acc).
|
||||
|
||||
has_extend_flag([]) -> false;
|
||||
has_extend_flag(["--extend" | _]) -> true;
|
||||
has_extend_flag([_ | Rest]) -> has_extend_flag(Rest).
|
||||
|
|
|
|||
50
un.ex
50
un.ex
|
|
@ -137,11 +137,13 @@ defmodule Un do
|
|||
shell = get_opt(args, "--shell", "-s", "bash")
|
||||
network = get_opt(args, "-n", nil, nil)
|
||||
vcpu = get_opt(args, "-v", nil, nil)
|
||||
input_files = get_all_opts(args, "-f")
|
||||
|
||||
network_json = if network, do: ",\"network\":\"#{network}\"", else: ""
|
||||
vcpu_json = if vcpu, do: ",\"vcpu\":#{vcpu}", else: ""
|
||||
input_files_json = build_input_files_json(input_files)
|
||||
|
||||
json = "{\"shell\":\"#{shell}\"#{network_json}#{vcpu_json}}"
|
||||
json = "{\"shell\":\"#{shell}\"#{network_json}#{vcpu_json}#{input_files_json}}"
|
||||
response = curl_post(api_key, "/sessions", json)
|
||||
IO.puts("#{@yellow}Session created (WebSocket required)#{@reset}")
|
||||
IO.puts(response)
|
||||
|
|
@ -238,17 +240,30 @@ defmodule Un do
|
|||
api_key = get_api_key()
|
||||
ports = get_opt(args, "--ports", nil, nil)
|
||||
bootstrap = get_opt(args, "--bootstrap", nil, nil)
|
||||
bootstrap_file = get_opt(args, "--bootstrap-file", nil, nil)
|
||||
network = get_opt(args, "-n", nil, nil)
|
||||
vcpu = get_opt(args, "-v", nil, nil)
|
||||
service_type = get_opt(args, "--type", nil, nil)
|
||||
input_files = get_all_opts(args, "-f")
|
||||
|
||||
ports_json = if ports, do: ",\"ports\":[#{ports}]", else: ""
|
||||
bootstrap_json = if bootstrap, do: ",\"bootstrap\":\"#{escape_json(bootstrap)}\"", else: ""
|
||||
bootstrap_content_json = if bootstrap_file do
|
||||
case File.read(bootstrap_file) do
|
||||
{:ok, content} -> ",\"bootstrap_content\":\"#{escape_json(content)}\""
|
||||
{:error, _} ->
|
||||
IO.puts(:stderr, "#{@red}Error: Bootstrap file not found: #{bootstrap_file}#{@reset}")
|
||||
System.halt(1)
|
||||
end
|
||||
else
|
||||
""
|
||||
end
|
||||
network_json = if network, do: ",\"network\":\"#{network}\"", else: ""
|
||||
vcpu_json = if vcpu, do: ",\"vcpu\":#{vcpu}", else: ""
|
||||
type_json = if service_type, do: ",\"service_type\":\"#{service_type}\"", else: ""
|
||||
input_files_json = build_input_files_json(input_files)
|
||||
|
||||
json = "{\"name\":\"#{name}\"#{ports_json}#{bootstrap_json}#{network_json}#{vcpu_json}#{type_json}}"
|
||||
json = "{\"name\":\"#{name}\"#{ports_json}#{bootstrap_json}#{bootstrap_content_json}#{network_json}#{vcpu_json}#{type_json}#{input_files_json}}"
|
||||
response = curl_post(api_key, "/services", json)
|
||||
IO.puts("#{@green}Service created#{@reset}")
|
||||
IO.puts(response)
|
||||
|
|
@ -443,6 +458,25 @@ defmodule Un do
|
|||
|> String.replace("\t", "\\t")
|
||||
end
|
||||
|
||||
defp read_and_base64(filepath) do
|
||||
case File.read(filepath) do
|
||||
{:ok, content} -> Base.encode64(content)
|
||||
{:error, _} -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp build_input_files_json([]), do: ""
|
||||
defp build_input_files_json(files) do
|
||||
file_jsons = files
|
||||
|> Enum.map(fn f ->
|
||||
b64 = read_and_base64(f)
|
||||
basename = Path.basename(f)
|
||||
"{\"filename\":\"#{escape_json(basename)}\",\"content\":\"#{b64}\"}"
|
||||
end)
|
||||
|> Enum.join(",")
|
||||
",\"input_files\":[#{file_jsons}]"
|
||||
end
|
||||
|
||||
defp build_execute_json(language, code, _opts) do
|
||||
"{\"language\":\"#{language}\",\"code\":\"#{escape_json(code)}\"}"
|
||||
end
|
||||
|
|
@ -559,6 +593,18 @@ defmodule Un do
|
|||
get_opt(rest, long, short, default)
|
||||
end
|
||||
|
||||
defp get_all_opts(args, flag), do: get_all_opts(args, flag, [])
|
||||
|
||||
defp get_all_opts([], _flag, acc), do: Enum.reverse(acc)
|
||||
|
||||
defp get_all_opts([arg, value | rest], flag, acc) when arg == flag do
|
||||
get_all_opts(rest, flag, [value | acc])
|
||||
end
|
||||
|
||||
defp get_all_opts([_arg | rest], flag, acc) do
|
||||
get_all_opts(rest, flag, acc)
|
||||
end
|
||||
|
||||
defp check_clock_drift(response) do
|
||||
response_lower = String.downcase(response)
|
||||
|
||||
|
|
|
|||
102
un.f90
102
un.f90
|
|
@ -192,15 +192,16 @@ contains
|
|||
end subroutine handle_execute
|
||||
|
||||
subroutine handle_session()
|
||||
character(len=4096) :: full_cmd
|
||||
character(len=8192) :: full_cmd
|
||||
character(len=256) :: arg, session_id
|
||||
character(len=1024) :: public_key, secret_key
|
||||
character(len=1024) :: public_key, secret_key, input_files
|
||||
integer :: i, stat
|
||||
logical :: list_mode, kill_mode
|
||||
|
||||
list_mode = .false.
|
||||
kill_mode = .false.
|
||||
session_id = ''
|
||||
input_files = ''
|
||||
|
||||
! Parse session arguments
|
||||
do i = 2, command_argument_count()
|
||||
|
|
@ -212,6 +213,15 @@ contains
|
|||
if (i+1 <= command_argument_count()) then
|
||||
call get_command_argument(i+1, session_id)
|
||||
end if
|
||||
else if (trim(arg) == '-f') then
|
||||
if (i+1 <= command_argument_count()) then
|
||||
call get_command_argument(i+1, arg)
|
||||
if (len_trim(input_files) > 0) then
|
||||
input_files = trim(input_files) // ',' // trim(arg)
|
||||
else
|
||||
input_files = trim(arg)
|
||||
end if
|
||||
end if
|
||||
end if
|
||||
end do
|
||||
|
||||
|
|
@ -258,14 +268,48 @@ contains
|
|||
'echo -e "\x1b[32mSession terminated: ', trim(session_id), '\x1b[0m"'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else
|
||||
write(0, '(A)') 'Error: Use --list or --kill ID'
|
||||
stop 1
|
||||
! Create session with optional input_files
|
||||
if (len_trim(input_files) > 0) then
|
||||
write(full_cmd, '(30A)') &
|
||||
'INPUT_FILES=""; ', &
|
||||
'IFS='','' read -ra FILES <<< "', trim(input_files), '"; ', &
|
||||
'for f in "${FILES[@]}"; do ', &
|
||||
'b64=$(base64 -w0 "$f" 2>/dev/null || base64 "$f"); ', &
|
||||
'name=$(basename "$f"); ', &
|
||||
'if [ -n "$INPUT_FILES" ]; then INPUT_FILES="$INPUT_FILES,"; fi; ', &
|
||||
'INPUT_FILES="$INPUT_FILES{\"filename\":\"$name\",\"content\":\"$b64\"}"; ', &
|
||||
'done; ', &
|
||||
'BODY=''{"shell":"bash","input_files":[''"$INPUT_FILES"'']}''; ', &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:POST:/sessions:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/sessions ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" ', &
|
||||
'-d "$BODY" && ', &
|
||||
'echo -e "\x1b[33mSession created (WebSocket required)\x1b[0m"'
|
||||
else
|
||||
write(full_cmd, '(20A)') &
|
||||
'BODY=''{"shell":"bash"}''; ', &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:POST:/sessions:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/sessions ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" ', &
|
||||
'-d "$BODY" && ', &
|
||||
'echo -e "\x1b[33mSession created (WebSocket required)\x1b[0m"'
|
||||
end if
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
end if
|
||||
end subroutine handle_session
|
||||
|
||||
subroutine handle_service()
|
||||
character(len=2048) :: full_cmd
|
||||
character(len=256) :: arg, service_id, operation, service_type
|
||||
character(len=8192) :: full_cmd
|
||||
character(len=256) :: arg, service_id, operation, service_type, service_name
|
||||
character(len=1024) :: input_files
|
||||
integer :: i, stat
|
||||
logical :: list_mode
|
||||
|
||||
|
|
@ -273,16 +317,32 @@ contains
|
|||
operation = ''
|
||||
service_id = ''
|
||||
service_type = ''
|
||||
service_name = ''
|
||||
input_files = ''
|
||||
|
||||
! Parse service arguments
|
||||
do i = 2, command_argument_count()
|
||||
call get_command_argument(i, arg)
|
||||
if (trim(arg) == '-l' .or. trim(arg) == '--list') then
|
||||
list_mode = .true.
|
||||
else if (trim(arg) == '--name') then
|
||||
operation = 'create'
|
||||
if (i+1 <= command_argument_count()) then
|
||||
call get_command_argument(i+1, service_name)
|
||||
end if
|
||||
else if (trim(arg) == '--type') then
|
||||
if (i+1 <= command_argument_count()) then
|
||||
call get_command_argument(i+1, service_type)
|
||||
end if
|
||||
else if (trim(arg) == '-f') then
|
||||
if (i+1 <= command_argument_count()) then
|
||||
call get_command_argument(i+1, arg)
|
||||
if (len_trim(input_files) > 0) then
|
||||
input_files = trim(input_files) // ',' // trim(arg)
|
||||
else
|
||||
input_files = trim(arg)
|
||||
end if
|
||||
end if
|
||||
else if (trim(arg) == '--info') then
|
||||
operation = 'info'
|
||||
if (i+1 <= command_argument_count()) then
|
||||
|
|
@ -385,8 +445,36 @@ contains
|
|||
'else echo "$STDOUT"; fi; ', &
|
||||
'else echo -e "\x1b[31mError: Failed to fetch bootstrap\x1b[0m" >&2; exit 1; fi'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (trim(operation) == 'create' .and. len_trim(service_name) > 0) then
|
||||
! Create service with optional input_files
|
||||
if (len_trim(input_files) > 0) then
|
||||
write(full_cmd, '(30A)') &
|
||||
'INPUT_FILES=""; ', &
|
||||
'IFS='','' read -ra FILES <<< "', trim(input_files), '"; ', &
|
||||
'for f in "${FILES[@]}"; do ', &
|
||||
'b64=$(base64 -w0 "$f" 2>/dev/null || base64 "$f"); ', &
|
||||
'name=$(basename "$f"); ', &
|
||||
'if [ -n "$INPUT_FILES" ]; then INPUT_FILES="$INPUT_FILES,"; fi; ', &
|
||||
'INPUT_FILES="$INPUT_FILES{\"filename\":\"$name\",\"content\":\"$b64\"}"; ', &
|
||||
'done; ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/services ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-H "Authorization: Bearer ', trim(api_key), '" ', &
|
||||
'-d ''{"name":"', trim(service_name), '","input_files":[''"$INPUT_FILES"'']}'' | ', &
|
||||
'jq -r ''.id + " created"'' && ', &
|
||||
'echo -e "\x1b[32mService created\x1b[0m"'
|
||||
else
|
||||
write(full_cmd, '(15A)') &
|
||||
'curl -s -X POST https://api.unsandbox.com/services ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-H "Authorization: Bearer ', trim(api_key), '" ', &
|
||||
'-d ''{"name":"', trim(service_name), '"}'' | ', &
|
||||
'jq -r ''.id + " created"'' && ', &
|
||||
'echo -e "\x1b[32mService created\x1b[0m"'
|
||||
end if
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else
|
||||
write(0, '(A)') 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --dump-bootstrap'
|
||||
write(0, '(A)') 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, --dump-bootstrap, or --name NAME'
|
||||
stop 1
|
||||
end if
|
||||
end subroutine handle_service
|
||||
|
|
|
|||
103
un.forth
103
un.forth
|
|
@ -400,7 +400,7 @@
|
|||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Service create (requires --name, optional --ports, --domains, --type, --bootstrap)
|
||||
\ Service create (requires --name, optional --ports, --domains, --type, --bootstrap, -f)
|
||||
: service-create ( -- )
|
||||
get-api-key
|
||||
\ Parse arguments (simplified - in real implementation would iterate through args)
|
||||
|
|
@ -413,15 +413,32 @@
|
|||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" NAME=''; PORTS=''; DOMAINS=''; TYPE=''; BOOTSTRAP=''" r@ write-line throw
|
||||
s" for ((i=3; i<$#; i+=2)); do" r@ write-line throw
|
||||
s" case ${!i} in" r@ write-line throw
|
||||
s" --name) NAME=${!((i+1))} ;;" r@ write-line throw
|
||||
s" --ports) PORTS=${!((i+1))} ;;" r@ write-line throw
|
||||
s" --domains) DOMAINS=${!((i+1))} ;;" r@ write-line throw
|
||||
s" --type) TYPE=${!((i+1))} ;;" r@ write-line throw
|
||||
s" --bootstrap) BOOTSTRAP=${!((i+1))} ;;" r@ write-line throw
|
||||
s" NAME=''; PORTS=''; DOMAINS=''; TYPE=''; BOOTSTRAP=''; BOOTSTRAP_FILE=''; INPUT_FILES=''" r@ write-line throw
|
||||
s" i=3" r@ write-line throw
|
||||
s" while [ $i -lt $# ]; do" r@ write-line throw
|
||||
s" arg=${!i}" r@ write-line throw
|
||||
s" case \"$arg\" in" r@ write-line throw
|
||||
s" --name) ((i++)); NAME=${!i} ;;" r@ write-line throw
|
||||
s" --ports) ((i++)); PORTS=${!i} ;;" r@ write-line throw
|
||||
s" --domains) ((i++)); DOMAINS=${!i} ;;" r@ write-line throw
|
||||
s" --type) ((i++)); TYPE=${!i} ;;" r@ write-line throw
|
||||
s" --bootstrap) ((i++)); BOOTSTRAP=${!i} ;;" r@ write-line throw
|
||||
s" --bootstrap-file) ((i++)); BOOTSTRAP_FILE=${!i} ;;" r@ write-line throw
|
||||
s" -f) ((i++)); FILE=${!i}" r@ write-line throw
|
||||
s" if [ -f \"$FILE\" ]; then" r@ write-line throw
|
||||
s" BASENAME=$(basename \"$FILE\")" r@ write-line throw
|
||||
s" CONTENT=$(base64 -w0 \"$FILE\")" r@ write-line throw
|
||||
s" if [ -z \"$INPUT_FILES\" ]; then" r@ write-line throw
|
||||
s" INPUT_FILES=\"{\\\"filename\\\":\\\"$BASENAME\\\",\\\"content\\\":\\\"$CONTENT\\\"}\"" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" INPUT_FILES=\"$INPUT_FILES,{\\\"filename\\\":\\\"$BASENAME\\\",\\\"content\\\":\\\"$CONTENT\\\"}\"" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" echo \"Error: File not found: $FILE\" >&2" r@ write-line throw
|
||||
s" exit 1" r@ write-line throw
|
||||
s" fi ;;" r@ write-line throw
|
||||
s" esac" r@ write-line throw
|
||||
s" ((i++))" r@ write-line throw
|
||||
s" done" r@ write-line throw
|
||||
s" [ -z \"$NAME\" ] && echo 'Error: --name required' && exit 1" r@ write-line throw
|
||||
s" PAYLOAD='{\"name\":\"'\"$NAME\"'\"}'" r@ write-line throw
|
||||
|
|
@ -429,6 +446,13 @@
|
|||
s" [ -n \"$DOMAINS\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg d \"$DOMAINS\" '. + {domains: ($d | split(\",\"))}')" r@ write-line throw
|
||||
s" [ -n \"$TYPE\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg t \"$TYPE\" '. + {service_type: $t}')" r@ write-line throw
|
||||
s" [ -n \"$BOOTSTRAP\" ] && PAYLOAD=$(echo $PAYLOAD | jq --arg b \"$BOOTSTRAP\" '. + {bootstrap: $b}')" r@ write-line throw
|
||||
s" if [ -n \"$BOOTSTRAP_FILE\" ]; then" r@ write-line throw
|
||||
s" [ ! -f \"$BOOTSTRAP_FILE\" ] && echo -e '\\x1b[31mError: Bootstrap file not found: '$BOOTSTRAP_FILE'\\x1b[0m' >&2 && exit 1" r@ write-line throw
|
||||
s" PAYLOAD=$(echo $PAYLOAD | jq --rawfile b \"$BOOTSTRAP_FILE\" '. + {bootstrap_content: $b}')" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" if [ -n \"$INPUT_FILES\" ]; then" r@ write-line throw
|
||||
s" PAYLOAD=$(echo $PAYLOAD | jq --argjson f \"[$INPUT_FILES]\" '. + {input_files: $f}')" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/services:$PAYLOAD\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
|
|
@ -515,6 +539,51 @@
|
|||
0 (bye)
|
||||
;
|
||||
|
||||
\ Session create with input_files support
|
||||
: session-create ( -- )
|
||||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" PUBLIC_KEY='" r@ write-file throw
|
||||
get-public-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SECRET_KEY='" r@ write-file throw
|
||||
get-secret-key r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" SHELL='bash'" r@ write-line throw
|
||||
s" INPUT_FILES=''" r@ write-line throw
|
||||
s" for ((i=2; i<$#; i++)); do" r@ write-line throw
|
||||
s" case ${!i} in" r@ write-line throw
|
||||
s" --shell|-s) ((i++)); SHELL=${!i} ;;" r@ write-line throw
|
||||
s" -f) ((i++)); FILE=${!i}" r@ write-line throw
|
||||
s" if [ -f \"$FILE\" ]; then" r@ write-line throw
|
||||
s" BASENAME=$(basename \"$FILE\")" r@ write-line throw
|
||||
s" CONTENT=$(base64 -w0 \"$FILE\")" r@ write-line throw
|
||||
s" if [ -z \"$INPUT_FILES\" ]; then" r@ write-line throw
|
||||
s" INPUT_FILES=\"{\\\"filename\\\":\\\"$BASENAME\\\",\\\"content\\\":\\\"$CONTENT\\\"}\"" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" INPUT_FILES=\"$INPUT_FILES,{\\\"filename\\\":\\\"$BASENAME\\\",\\\"content\\\":\\\"$CONTENT\\\"}\"" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" echo \"Error: File not found: $FILE\" >&2" r@ write-line throw
|
||||
s" exit 1" r@ write-line throw
|
||||
s" fi ;;" r@ write-line throw
|
||||
s" esac" r@ write-line throw
|
||||
s" done" r@ write-line throw
|
||||
s" if [ -n \"$INPUT_FILES\" ]; then" r@ write-line throw
|
||||
s" BODY=\"{\\\"shell\\\":\\\"$SHELL\\\",\\\"input_files\\\":[$INPUT_FILES]}\"" r@ write-line throw
|
||||
s" else" r@ write-line throw
|
||||
s" BODY=\"{\\\"shell\\\":\\\"$SHELL\\\"}\"" r@ write-line throw
|
||||
s" fi" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/sessions:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" echo -e '\\x1b[33mCreating session...\\x1b[0m'" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/sessions -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\"" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && bash /tmp/unsandbox_cmd.sh \"$@\" && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
||||
\ Handle session subcommand
|
||||
: handle-session ( -- )
|
||||
argc @ 3 < if
|
||||
|
|
@ -542,6 +611,22 @@
|
|||
0 (bye)
|
||||
then
|
||||
|
||||
\ Check for --shell or -f flags (create session)
|
||||
2dup s" --shell" compare 0= if
|
||||
2drop session-create
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" -s" compare 0= if
|
||||
2drop session-create
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2dup s" -f" compare 0= if
|
||||
2drop session-create
|
||||
0 (bye)
|
||||
then
|
||||
|
||||
2drop
|
||||
s" Error: Use --list or --kill ID" type cr
|
||||
1 (bye)
|
||||
|
|
|
|||
23
un.fs
23
un.fs
|
|
@ -89,6 +89,7 @@ type Args = {
|
|||
mutable ServicePorts: string option
|
||||
mutable ServiceType: string option
|
||||
mutable ServiceBootstrap: string option
|
||||
mutable ServiceBootstrapFile: string option
|
||||
mutable ServiceInfo: string option
|
||||
mutable ServiceLogs: string option
|
||||
mutable ServiceTail: string option
|
||||
|
|
@ -358,6 +359,13 @@ let cmdSession (args: Args) =
|
|||
if args.Vcpu > 0 then
|
||||
payload <- payload @ [("vcpu", box args.Vcpu)]
|
||||
|
||||
if args.Files.Count > 0 then
|
||||
let inputFiles = args.Files |> Seq.map (fun filepath ->
|
||||
let content = File.ReadAllBytes(filepath)
|
||||
[("filename", box (Path.GetFileName(filepath))); ("content_base64", box (Convert.ToBase64String(content)))]
|
||||
) |> Seq.toList
|
||||
payload <- payload @ [("input_files", box inputFiles)]
|
||||
|
||||
printfn "%sCreating session...%s" yellow reset
|
||||
let result = apiRequest "/sessions" "POST" (Some payload) publicKey secretKey
|
||||
match result.TryFind "id" with
|
||||
|
|
@ -526,6 +534,19 @@ let cmdService (args: Args) =
|
|||
payload <- payload @ [("service_type", box args.ServiceType.Value)]
|
||||
if args.ServiceBootstrap.IsSome then
|
||||
payload <- payload @ [("bootstrap", box args.ServiceBootstrap.Value)]
|
||||
if args.ServiceBootstrapFile.IsSome then
|
||||
if File.Exists(args.ServiceBootstrapFile.Value) then
|
||||
let content = File.ReadAllText(args.ServiceBootstrapFile.Value)
|
||||
payload <- payload @ [("bootstrap_content", box content)]
|
||||
else
|
||||
eprintfn "%sError: Bootstrap file not found: %s%s" red args.ServiceBootstrapFile.Value reset
|
||||
exit 1
|
||||
if args.Files.Count > 0 then
|
||||
let inputFiles = args.Files |> Seq.map (fun filepath ->
|
||||
let content = File.ReadAllBytes(filepath)
|
||||
[("filename", box (Path.GetFileName(filepath))); ("content_base64", box (Convert.ToBase64String(content)))]
|
||||
) |> Seq.toList
|
||||
payload <- payload @ [("input_files", box inputFiles)]
|
||||
if args.Network.IsSome then
|
||||
payload <- payload @ [("network", box args.Network.Value)]
|
||||
if args.Vcpu > 0 then
|
||||
|
|
@ -564,6 +585,7 @@ let parseArgs (argv: string[]) =
|
|||
ServicePorts = None
|
||||
ServiceType = None
|
||||
ServiceBootstrap = None
|
||||
ServiceBootstrapFile = None
|
||||
ServiceInfo = None
|
||||
ServiceLogs = None
|
||||
ServiceTail = None
|
||||
|
|
@ -601,6 +623,7 @@ let parseArgs (argv: string[]) =
|
|||
| "--ports" -> i <- i + 1; args.ServicePorts <- Some argv.[i]
|
||||
| "--type" -> i <- i + 1; args.ServiceType <- Some argv.[i]
|
||||
| "--bootstrap" -> i <- i + 1; args.ServiceBootstrap <- Some argv.[i]
|
||||
| "--bootstrap-file" -> i <- i + 1; args.ServiceBootstrapFile <- Some argv.[i]
|
||||
| "--info" -> i <- i + 1; args.ServiceInfo <- Some argv.[i]
|
||||
| "--logs" -> i <- i + 1; args.ServiceLogs <- Some argv.[i]
|
||||
| "--tail" -> i <- i + 1; args.ServiceTail <- Some argv.[i]
|
||||
|
|
|
|||
62
un.go
62
un.go
|
|
@ -330,7 +330,7 @@ func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts boo
|
|||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int, tmux, screen bool, publicKey, secretKey string) {
|
||||
func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int, tmux, screen bool, files inputFiles, publicKey, secretKey string) {
|
||||
if sessionList != "" {
|
||||
result := apiRequest("/sessions", "GET", nil, publicKey, secretKey)
|
||||
sessions := result["sessions"].([]interface{})
|
||||
|
|
@ -373,12 +373,29 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
|
|||
payload["persistence"] = "screen"
|
||||
}
|
||||
|
||||
// Input files
|
||||
if len(files) > 0 {
|
||||
var inputFilesList []map[string]string
|
||||
for _, f := range files {
|
||||
content, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError reading input file %s: %v%s\n", Red, f, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
inputFilesList = append(inputFilesList, map[string]string{
|
||||
"filename": filepath.Base(f),
|
||||
"content_base64": base64.StdEncoding.EncodeToString(content),
|
||||
})
|
||||
}
|
||||
payload["input_files"] = inputFilesList
|
||||
}
|
||||
|
||||
fmt.Printf("%sCreating session...%s\n", Yellow, Reset)
|
||||
result := apiRequest("/sessions", "POST", payload, publicKey, secretKey)
|
||||
fmt.Printf("%sSession created: %s%s\n", Green, result["id"], Reset)
|
||||
}
|
||||
|
||||
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, network string, vcpu int, publicKey, secretKey string) {
|
||||
func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceBootstrap, serviceBootstrapFile, serviceList, serviceInfo, serviceLogs, serviceTail, serviceSleep, serviceWake, serviceDestroy, serviceExecute, serviceCommand, serviceDumpBootstrap, serviceDumpFile, network string, vcpu int, files inputFiles, publicKey, secretKey string) {
|
||||
if serviceList != "" {
|
||||
result := apiRequest("/services", "GET", nil, publicKey, secretKey)
|
||||
services := result["services"].([]interface{})
|
||||
|
|
@ -503,13 +520,31 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
|
|||
payload["service_type"] = serviceType
|
||||
}
|
||||
if serviceBootstrap != "" {
|
||||
// Check if it's a file
|
||||
if _, err := os.Stat(serviceBootstrap); err == nil {
|
||||
content, _ := os.ReadFile(serviceBootstrap)
|
||||
payload["bootstrap"] = string(content)
|
||||
} else {
|
||||
payload["bootstrap"] = serviceBootstrap
|
||||
payload["bootstrap"] = serviceBootstrap
|
||||
}
|
||||
if serviceBootstrapFile != "" {
|
||||
content, err := os.ReadFile(serviceBootstrapFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError: Bootstrap file not found: %s%s\n", Red, serviceBootstrapFile, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
payload["bootstrap_content"] = string(content)
|
||||
}
|
||||
// Input files
|
||||
if len(files) > 0 {
|
||||
var inputFilesList []map[string]string
|
||||
for _, f := range files {
|
||||
content, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError reading input file %s: %v%s\n", Red, f, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
inputFilesList = append(inputFilesList, map[string]string{
|
||||
"filename": filepath.Base(f),
|
||||
"content_base64": base64.StdEncoding.EncodeToString(content),
|
||||
})
|
||||
}
|
||||
payload["input_files"] = inputFilesList
|
||||
}
|
||||
if network != "" {
|
||||
payload["network"] = network
|
||||
|
|
@ -706,6 +741,8 @@ func main() {
|
|||
sessionShell := sessionCmd.String("shell", "", "Shell/REPL to use")
|
||||
sessionTmux := sessionCmd.Bool("tmux", false, "Enable tmux persistence")
|
||||
sessionScreen := sessionCmd.Bool("screen", false, "Enable screen persistence")
|
||||
var sessionFiles inputFiles
|
||||
sessionCmd.Var(&sessionFiles, "f", "Input file")
|
||||
sessionNetwork := sessionCmd.String("n", "", "Network mode")
|
||||
sessionVcpu := sessionCmd.Int("v", 0, "vCPU count")
|
||||
sessionKey := sessionCmd.String("k", "", "API key")
|
||||
|
|
@ -716,7 +753,10 @@ func main() {
|
|||
servicePorts := serviceCmd.String("ports", "", "Ports (comma-separated)")
|
||||
serviceDomains := serviceCmd.String("domains", "", "Custom domains (comma-separated)")
|
||||
serviceType := serviceCmd.String("type", "", "Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)")
|
||||
serviceBootstrap := serviceCmd.String("bootstrap", "", "Bootstrap command/file")
|
||||
serviceBootstrap := serviceCmd.String("bootstrap", "", "Bootstrap command or URI")
|
||||
serviceBootstrapFile := serviceCmd.String("bootstrap-file", "", "Upload local file as bootstrap script")
|
||||
var serviceFiles inputFiles
|
||||
serviceCmd.Var(&serviceFiles, "f", "Input file")
|
||||
serviceList := serviceCmd.String("list", "", "List services")
|
||||
serviceInfo := serviceCmd.String("info", "", "Get service info")
|
||||
serviceLogs := serviceCmd.String("logs", "", "Get service logs")
|
||||
|
|
@ -753,7 +793,7 @@ func main() {
|
|||
if vc == 0 {
|
||||
vc = *vcpu
|
||||
}
|
||||
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, publicKey, secretKey)
|
||||
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, sessionFiles, publicKey, secretKey)
|
||||
return
|
||||
|
||||
case "service":
|
||||
|
|
@ -767,7 +807,7 @@ func main() {
|
|||
if vc == 0 {
|
||||
vc = *vcpu
|
||||
}
|
||||
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, net, vc, publicKey, secretKey)
|
||||
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceBootstrapFile, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, net, vc, serviceFiles, publicKey, secretKey)
|
||||
return
|
||||
|
||||
case "key":
|
||||
|
|
|
|||
44
un.groovy
44
un.groovy
|
|
@ -79,6 +79,7 @@ class Args {
|
|||
String servicePorts = null
|
||||
String serviceType = null
|
||||
String serviceBootstrap = null
|
||||
String serviceBootstrapFile = null
|
||||
String serviceInfo = null
|
||||
String serviceLogs = null
|
||||
String serviceTail = null
|
||||
|
|
@ -307,6 +308,21 @@ def cmdSession(args) {
|
|||
if (args.vcpu > 0) {
|
||||
json += ""","vcpu":${args.vcpu}"""
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if (args.files) {
|
||||
def filesJson = args.files.collect { filepath ->
|
||||
def f = new File(filepath)
|
||||
if (!f.exists()) {
|
||||
System.err.println("${RED}Error: Input file not found: ${filepath}${RESET}")
|
||||
System.exit(1)
|
||||
}
|
||||
def content = f.bytes.encodeBase64().toString()
|
||||
return """{"filename":"${f.name}","content_base64":"${content}"}"""
|
||||
}.join(',')
|
||||
json += ""","input_files":[${filesJson}]"""
|
||||
}
|
||||
|
||||
json += '}'
|
||||
|
||||
println("${YELLOW}Creating session...${RESET}")
|
||||
|
|
@ -539,12 +555,37 @@ def cmdService(args) {
|
|||
def escaped = args.serviceBootstrap.replace('\\', '\\\\').replace('"', '\\"')
|
||||
json += ""","bootstrap":"${escaped}""""
|
||||
}
|
||||
if (args.serviceBootstrapFile) {
|
||||
def file = new File(args.serviceBootstrapFile)
|
||||
if (file.exists()) {
|
||||
def content = file.text.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n').replace('\r', '\\r').replace('\t', '\\t')
|
||||
json += ""","bootstrap_content":"${content}""""
|
||||
} else {
|
||||
System.err.println("${RED}Error: Bootstrap file not found: ${args.serviceBootstrapFile}${RESET}")
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
if (args.network) {
|
||||
json += ""","network":"${args.network}""""
|
||||
}
|
||||
if (args.vcpu > 0) {
|
||||
json += ""","vcpu":${args.vcpu}"""
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if (args.files) {
|
||||
def filesJson = args.files.collect { filepath ->
|
||||
def f = new File(filepath)
|
||||
if (!f.exists()) {
|
||||
System.err.println("${RED}Error: Input file not found: ${filepath}${RESET}")
|
||||
System.exit(1)
|
||||
}
|
||||
def content = f.bytes.encodeBase64().toString()
|
||||
return """{"filename":"${f.name}","content_base64":"${content}"}"""
|
||||
}.join(',')
|
||||
json += ""","input_files":[${filesJson}]"""
|
||||
}
|
||||
|
||||
json += '}'
|
||||
|
||||
def output = apiRequest('/services', 'POST', json, publicKey, secretKey)
|
||||
|
|
@ -633,6 +674,9 @@ def parseArgs(argv) {
|
|||
case '--bootstrap':
|
||||
args.serviceBootstrap = argv[++i]
|
||||
break
|
||||
case '--bootstrap-file':
|
||||
args.serviceBootstrapFile = argv[++i]
|
||||
break
|
||||
case '--info':
|
||||
args.serviceInfo = argv[++i]
|
||||
break
|
||||
|
|
|
|||
41
un.hs
41
un.hs
|
|
@ -133,6 +133,7 @@ data SessionOpts = SessionOpts
|
|||
, sessShell :: Maybe String
|
||||
, sessNetwork :: Maybe String
|
||||
, sessVcpu :: Maybe Int
|
||||
, sessFiles :: [String]
|
||||
}
|
||||
|
||||
data SessionAction = SessionList | SessionKill String | SessionCreate
|
||||
|
|
@ -143,8 +144,10 @@ data ServiceOpts = ServiceOpts
|
|||
, svcPorts :: Maybe String
|
||||
, svcType :: Maybe String
|
||||
, svcBootstrap :: Maybe String
|
||||
, svcBootstrapFile :: Maybe String
|
||||
, svcNetwork :: Maybe String
|
||||
, svcVcpu :: Maybe Int
|
||||
, svcFiles :: [String]
|
||||
}
|
||||
|
||||
data ServiceAction = ServiceList | ServiceInfo String | ServiceLogs String
|
||||
|
|
@ -174,7 +177,7 @@ parseKey args = return $ parseKeyArgs args defaultKeyOpts
|
|||
parseSession :: [String] -> IO SessionOpts
|
||||
parseSession args = return $ parseSessionArgs args defaultSessionOpts
|
||||
where
|
||||
defaultSessionOpts = SessionOpts SessionCreate Nothing Nothing Nothing
|
||||
defaultSessionOpts = SessionOpts SessionCreate Nothing Nothing Nothing []
|
||||
parseSessionArgs [] opts = opts
|
||||
parseSessionArgs ("--list":rest) opts = parseSessionArgs rest opts { sessAction = SessionList }
|
||||
parseSessionArgs ("--kill":id:rest) opts = parseSessionArgs rest opts { sessAction = SessionKill id }
|
||||
|
|
@ -182,12 +185,13 @@ parseSession args = return $ parseSessionArgs args defaultSessionOpts
|
|||
parseSessionArgs ("-s":sh:rest) opts = parseSessionArgs rest opts { sessShell = Just sh }
|
||||
parseSessionArgs ("-n":net:rest) opts = parseSessionArgs rest opts { sessNetwork = Just net }
|
||||
parseSessionArgs ("-v":v:rest) opts = parseSessionArgs rest opts { sessVcpu = Just (read v) }
|
||||
parseSessionArgs ("-f":f:rest) opts = parseSessionArgs rest opts { sessFiles = sessFiles opts ++ [f] }
|
||||
parseSessionArgs (_:rest) opts = parseSessionArgs rest opts
|
||||
|
||||
parseService :: [String] -> IO ServiceOpts
|
||||
parseService args = return $ parseServiceArgs args defaultServiceOpts
|
||||
where
|
||||
defaultServiceOpts = ServiceOpts ServiceCreate Nothing Nothing Nothing Nothing Nothing Nothing
|
||||
defaultServiceOpts = ServiceOpts ServiceCreate Nothing Nothing Nothing Nothing Nothing Nothing Nothing []
|
||||
parseServiceArgs [] opts = opts
|
||||
parseServiceArgs ("--list":rest) opts = parseServiceArgs rest opts { svcAction = ServiceList }
|
||||
parseServiceArgs ("--info":id:rest) opts = parseServiceArgs rest opts { svcAction = ServiceInfo id }
|
||||
|
|
@ -202,8 +206,10 @@ parseService args = return $ parseServiceArgs args defaultServiceOpts
|
|||
parseServiceArgs ("--ports":p:rest) opts = parseServiceArgs rest opts { svcPorts = Just p }
|
||||
parseServiceArgs ("--type":t:rest) opts = parseServiceArgs rest opts { svcType = Just t }
|
||||
parseServiceArgs ("--bootstrap":b:rest) opts = parseServiceArgs rest opts { svcBootstrap = Just b }
|
||||
parseServiceArgs ("--bootstrap-file":f:rest) opts = parseServiceArgs rest opts { svcBootstrapFile = Just f }
|
||||
parseServiceArgs ("-n":net:rest) opts = parseServiceArgs rest opts { svcNetwork = Just net }
|
||||
parseServiceArgs ("-v":v:rest) opts = parseServiceArgs rest opts { svcVcpu = Just (read v) }
|
||||
parseServiceArgs ("-f":f:rest) opts = parseServiceArgs rest opts { svcFiles = svcFiles opts ++ [f] }
|
||||
parseServiceArgs (_:rest) opts = parseServiceArgs rest opts
|
||||
|
||||
parseExecute :: [String] -> IO Command
|
||||
|
|
@ -317,7 +323,18 @@ sessionCommand opts = do
|
|||
let shell = maybe "bash" id (sessShell opts)
|
||||
let networkJSON = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") (sessNetwork opts)
|
||||
let vcpuJSON = maybe "" (\v -> ",\"vcpu\":" ++ show v) (sessVcpu opts)
|
||||
let json = "{\"shell\":\"" ++ shell ++ "\"" ++ networkJSON ++ vcpuJSON ++ "}"
|
||||
-- Input files
|
||||
filesJSON <- if null (sessFiles opts)
|
||||
then return ""
|
||||
else do
|
||||
fileEntries <- mapM (\f -> do
|
||||
content <- BS.readFile f
|
||||
let b64 = BSC.unpack $ B64.encode content
|
||||
let fname = takeFileName f
|
||||
return $ "{\"filename\":\"" ++ fname ++ "\",\"content_base64\":\"" ++ b64 ++ "\"}"
|
||||
) (sessFiles opts)
|
||||
return $ ",\"input_files\":[" ++ intercalate "," fileEntries ++ "]"
|
||||
let json = "{\"shell\":\"" ++ shell ++ "\"" ++ networkJSON ++ vcpuJSON ++ filesJSON ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey "https://api.unsandbox.com/sessions" json
|
||||
putStrLn $ yellow ++ "Session created (WebSocket required for interactivity)" ++ reset
|
||||
putStrLn stdout
|
||||
|
|
@ -376,9 +393,25 @@ serviceCommand opts = do
|
|||
let portsJSON = maybe "" (\p -> ",\"ports\":[" ++ p ++ "]") (svcPorts opts)
|
||||
let typeJSON = maybe "" (\t -> ",\"service_type\":\"" ++ t ++ "\"") (svcType opts)
|
||||
let bootstrapJSON = maybe "" (\b -> ",\"bootstrap\":\"" ++ escapeJSON b ++ "\"") (svcBootstrap opts)
|
||||
bootstrapContentJSON <- case svcBootstrapFile opts of
|
||||
Just f -> do
|
||||
content <- readFile f
|
||||
return $ ",\"bootstrap_content\":\"" ++ escapeJSON content ++ "\""
|
||||
Nothing -> return ""
|
||||
let networkJSON = maybe "" (\n -> ",\"network\":\"" ++ n ++ "\"") (svcNetwork opts)
|
||||
let vcpuJSON = maybe "" (\v -> ",\"vcpu\":" ++ show v) (svcVcpu opts)
|
||||
let json = "{\"name\":\"" ++ name ++ "\"" ++ portsJSON ++ typeJSON ++ bootstrapJSON ++ networkJSON ++ vcpuJSON ++ "}"
|
||||
-- Input files
|
||||
filesJSON <- if null (svcFiles opts)
|
||||
then return ""
|
||||
else do
|
||||
fileEntries <- mapM (\f -> do
|
||||
content <- BS.readFile f
|
||||
let b64 = BSC.unpack $ B64.encode content
|
||||
let fname = takeFileName f
|
||||
return $ "{\"filename\":\"" ++ fname ++ "\",\"content_base64\":\"" ++ b64 ++ "\"}"
|
||||
) (svcFiles opts)
|
||||
return $ ",\"input_files\":[" ++ intercalate "," fileEntries ++ "]"
|
||||
let json = "{\"name\":\"" ++ name ++ "\"" ++ portsJSON ++ typeJSON ++ bootstrapJSON ++ bootstrapContentJSON ++ networkJSON ++ vcpuJSON ++ filesJSON ++ "}"
|
||||
(_, stdout, _) <- curlPost apiKey "https://api.unsandbox.com/services" json
|
||||
putStrLn $ green ++ "Service created" ++ reset
|
||||
putStrLn stdout
|
||||
|
|
|
|||
78
un.jl
78
un.jl
|
|
@ -276,8 +276,36 @@ function cmd_session(args)
|
|||
return
|
||||
end
|
||||
|
||||
println(stderr, "$(RED)Error: Use --list or --kill$(RESET)")
|
||||
exit(1)
|
||||
# Create new session
|
||||
payload = Dict("shell" => "bash")
|
||||
|
||||
if args["network"] !== nothing
|
||||
payload["network"] = args["network"]
|
||||
end
|
||||
|
||||
# Add input files
|
||||
if args["files"] !== nothing
|
||||
input_files = []
|
||||
for filepath in args["files"]
|
||||
if !isfile(filepath)
|
||||
println(stderr, "$(RED)Error: Input file not found: $filepath$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
content = base64encode(read(filepath))
|
||||
push!(input_files, Dict(
|
||||
"filename" => basename(filepath),
|
||||
"content_base64" => content
|
||||
))
|
||||
end
|
||||
if !isempty(input_files)
|
||||
payload["input_files"] = input_files
|
||||
end
|
||||
end
|
||||
|
||||
println("$(YELLOW)Creating session...$(RESET)")
|
||||
result = api_request("/sessions", public_key, secret_key, method="POST", data=payload)
|
||||
println("$(GREEN)Session created: $(get(result, "id", "N/A"))$(RESET)")
|
||||
println("$(YELLOW)(Interactive sessions require WebSocket - use un2 for full support)$(RESET)")
|
||||
end
|
||||
|
||||
function cmd_service(args)
|
||||
|
|
@ -380,11 +408,16 @@ function cmd_service(args)
|
|||
end
|
||||
|
||||
if args["bootstrap"] !== nothing
|
||||
bootstrap = args["bootstrap"]
|
||||
if isfile(bootstrap)
|
||||
payload["bootstrap"] = read(bootstrap, String)
|
||||
payload["bootstrap"] = args["bootstrap"]
|
||||
end
|
||||
|
||||
if args["bootstrap-file"] !== nothing
|
||||
bootstrap_file = args["bootstrap-file"]
|
||||
if isfile(bootstrap_file)
|
||||
payload["bootstrap_content"] = read(bootstrap_file, String)
|
||||
else
|
||||
payload["bootstrap"] = bootstrap
|
||||
println(stderr, "$(RED)Error: Bootstrap file not found: $bootstrap_file$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -396,6 +429,25 @@ function cmd_service(args)
|
|||
payload["vcpu"] = args["vcpu"]
|
||||
end
|
||||
|
||||
# Add input files
|
||||
if args["files"] !== nothing
|
||||
input_files = []
|
||||
for filepath in args["files"]
|
||||
if !isfile(filepath)
|
||||
println(stderr, "$(RED)Error: Input file not found: $filepath$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
content = base64encode(read(filepath))
|
||||
push!(input_files, Dict(
|
||||
"filename" => basename(filepath),
|
||||
"content_base64" => content
|
||||
))
|
||||
end
|
||||
if !isempty(input_files)
|
||||
payload["input_files"] = input_files
|
||||
end
|
||||
end
|
||||
|
||||
result = api_request("/services", public_key, secret_key, method="POST", data=payload)
|
||||
println("$(GREEN)Service created: $(get(result, "id", "N/A"))$(RESET)")
|
||||
println("Name: $(get(result, "name", "N/A"))")
|
||||
|
|
@ -595,6 +647,13 @@ function main()
|
|||
action = :store_true
|
||||
"--kill"
|
||||
help = "Terminate session"
|
||||
"--files", "-f"
|
||||
help = "Add input file"
|
||||
action = :append_arg
|
||||
"--network", "-n"
|
||||
help = "Network mode"
|
||||
arg_type = String
|
||||
range_tester = x -> x in ["zerotrust", "semitrusted"]
|
||||
"--api-key", "-k"
|
||||
help = "API key"
|
||||
end
|
||||
|
|
@ -609,7 +668,12 @@ function main()
|
|||
"--type"
|
||||
help = "Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)"
|
||||
"--bootstrap"
|
||||
help = "Bootstrap command/file"
|
||||
help = "Bootstrap command or URI"
|
||||
"--bootstrap-file"
|
||||
help = "Upload local file as bootstrap script"
|
||||
"--files", "-f"
|
||||
help = "Add input file"
|
||||
action = :append_arg
|
||||
"--network", "-n"
|
||||
help = "Network mode"
|
||||
arg_type = String
|
||||
|
|
|
|||
48
un.js
48
un.js
|
|
@ -417,6 +417,22 @@ async function cmdSession(args) {
|
|||
if (args.screen) payload.persistence = "screen";
|
||||
if (args.audit) payload.audit = true;
|
||||
|
||||
// Add input files
|
||||
if (args.files && args.files.length > 0) {
|
||||
payload.input_files = args.files.map(filepath => {
|
||||
try {
|
||||
const content = fs.readFileSync(filepath);
|
||||
return {
|
||||
filename: path.basename(filepath),
|
||||
content_base64: content.toString('base64')
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`${YELLOW}Creating session...${RESET}`);
|
||||
const result = await apiRequest("/sessions", "POST", payload, publicKey, secretKey);
|
||||
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
|
||||
|
|
@ -520,11 +536,29 @@ async function cmdService(args) {
|
|||
if (args.domains) payload.domains = args.domains.split(',');
|
||||
if (args.serviceType) payload.service_type = args.serviceType;
|
||||
if (args.bootstrap) {
|
||||
if (fs.existsSync(args.bootstrap)) {
|
||||
payload.bootstrap = fs.readFileSync(args.bootstrap, 'utf-8');
|
||||
} else {
|
||||
payload.bootstrap = args.bootstrap;
|
||||
payload.bootstrap = args.bootstrap;
|
||||
}
|
||||
if (args.bootstrapFile) {
|
||||
if (!fs.existsSync(args.bootstrapFile)) {
|
||||
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
|
||||
}
|
||||
// Add input files
|
||||
if (args.files && args.files.length > 0) {
|
||||
payload.input_files = args.files.map(filepath => {
|
||||
try {
|
||||
const content = fs.readFileSync(filepath);
|
||||
return {
|
||||
filename: path.basename(filepath),
|
||||
content_base64: content.toString('base64')
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (args.network) payload.network = args.network;
|
||||
if (args.vcpu) payload.vcpu = args.vcpu;
|
||||
|
|
@ -639,6 +673,9 @@ function parseArgs(argv) {
|
|||
} else if (arg === '--bootstrap' && i + 1 < argv.length) {
|
||||
args.bootstrap = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--bootstrap-file' && i + 1 < argv.length) {
|
||||
args.bootstrapFile = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--info' && i + 1 < argv.length) {
|
||||
args.info = argv[++i];
|
||||
i++;
|
||||
|
|
@ -727,7 +764,8 @@ Service options:
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
42
un.kt
42
un.kt
|
|
@ -90,6 +90,7 @@ data class Args(
|
|||
var servicePorts: String? = null,
|
||||
var serviceType: String? = null,
|
||||
var serviceBootstrap: String? = null,
|
||||
var serviceBootstrapFile: String? = null,
|
||||
var serviceInfo: String? = null,
|
||||
var serviceLogs: String? = null,
|
||||
var serviceTail: String? = null,
|
||||
|
|
@ -238,6 +239,22 @@ fun cmdSession(args: Args) {
|
|||
payload["vcpu"] = args.vcpu
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if (args.files.isNotEmpty()) {
|
||||
val inputFiles = args.files.map { filepath ->
|
||||
val file = java.io.File(filepath)
|
||||
if (!file.exists()) {
|
||||
System.err.println("${RED}Error: Input file not found: $filepath${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
mapOf(
|
||||
"filename" to file.name,
|
||||
"content_base64" to java.util.Base64.getEncoder().encodeToString(file.readBytes())
|
||||
)
|
||||
}
|
||||
payload["input_files"] = inputFiles
|
||||
}
|
||||
|
||||
println("${YELLOW}Creating session...${RESET}")
|
||||
val result = apiRequest("/sessions", "POST", payload, publicKey, secretKey)
|
||||
println("${GREEN}Session created: ${result["id"] ?: "N/A"}${RESET}")
|
||||
|
|
@ -363,6 +380,30 @@ fun cmdService(args: Args) {
|
|||
if (args.serviceBootstrap != null) {
|
||||
payload["bootstrap"] = args.serviceBootstrap!!
|
||||
}
|
||||
if (args.serviceBootstrapFile != null) {
|
||||
val file = File(args.serviceBootstrapFile!!)
|
||||
if (file.exists()) {
|
||||
payload["bootstrap_content"] = file.readText()
|
||||
} else {
|
||||
System.err.println("${RED}Error: Bootstrap file not found: ${args.serviceBootstrapFile}${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
// Add input files
|
||||
if (args.files.isNotEmpty()) {
|
||||
val inputFiles = args.files.map { filepath ->
|
||||
val file = java.io.File(filepath)
|
||||
if (!file.exists()) {
|
||||
System.err.println("${RED}Error: Input file not found: $filepath${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
mapOf(
|
||||
"filename" to file.name,
|
||||
"content_base64" to java.util.Base64.getEncoder().encodeToString(file.readBytes())
|
||||
)
|
||||
}
|
||||
payload["input_files"] = inputFiles
|
||||
}
|
||||
if (args.network != null) {
|
||||
payload["network"] = args.network!!
|
||||
}
|
||||
|
|
@ -695,6 +736,7 @@ fun parseArgs(args: Array<String>): Args {
|
|||
"--ports" -> result.servicePorts = args[++i]
|
||||
"--type" -> result.serviceType = args[++i]
|
||||
"--bootstrap" -> result.serviceBootstrap = args[++i]
|
||||
"--bootstrap-file" -> result.serviceBootstrapFile = args[++i]
|
||||
"--info" -> result.serviceInfo = args[++i]
|
||||
"--logs" -> result.serviceLogs = args[++i]
|
||||
"--tail" -> result.serviceTail = args[++i]
|
||||
|
|
|
|||
95
un.lisp
95
un.lisp
|
|
@ -88,6 +88,33 @@
|
|||
(read-sequence contents stream)
|
||||
contents)))
|
||||
|
||||
(defun read-file-binary (filename)
|
||||
"Read file as binary and return as vector of bytes"
|
||||
(with-open-file (stream filename :element-type '(unsigned-byte 8))
|
||||
(let* ((len (file-length stream))
|
||||
(data (make-array len :element-type '(unsigned-byte 8))))
|
||||
(read-sequence data stream)
|
||||
data)))
|
||||
|
||||
(defun base64-encode-file (filename)
|
||||
"Base64 encode a file using shell command"
|
||||
(let* ((cmd (format nil "base64 -w0 ~a" (uiop:escape-sh-token filename)))
|
||||
(result (string-trim '(#\Space #\Tab #\Newline #\Return)
|
||||
(uiop:run-program cmd :output :string))))
|
||||
result))
|
||||
|
||||
(defun build-input-files-json (files)
|
||||
"Build input_files JSON array from list of filenames"
|
||||
(if (null files)
|
||||
""
|
||||
(format nil ",\"input_files\":[~{~a~^,~}]"
|
||||
(mapcar (lambda (f)
|
||||
(let* ((basename (file-namestring f))
|
||||
(content (base64-encode-file f)))
|
||||
(format nil "{\"filename\":\"~a\",\"content\":\"~a\"}"
|
||||
basename content)))
|
||||
files))))
|
||||
|
||||
(defun write-temp-file (data)
|
||||
(let ((tmp-file (format nil "/tmp/un_lisp_~a.json" (random 999999))))
|
||||
(with-open-file (stream tmp-file :direction :output :if-exists :supersede)
|
||||
|
|
@ -209,7 +236,7 @@
|
|||
(response (curl-post api-key "/execute" json)))
|
||||
(format t "~a~%" response))))
|
||||
|
||||
(defun session-cmd (action id shell)
|
||||
(defun session-cmd (action id shell input-files)
|
||||
(let ((api-key (get-api-key)))
|
||||
(cond
|
||||
((string= action "list")
|
||||
|
|
@ -219,12 +246,13 @@
|
|||
(format t "~aSession terminated: ~a~a~%" *green* id *reset*))
|
||||
(t
|
||||
(let* ((sh (or shell "bash"))
|
||||
(json (format nil "{\"shell\":\"~a\"}" sh))
|
||||
(input-files-json (build-input-files-json input-files))
|
||||
(json (format nil "{\"shell\":\"~a\"~a}" sh input-files-json))
|
||||
(response (curl-post api-key "/sessions" json)))
|
||||
(format t "~aSession created (WebSocket required)~a~%" *yellow* *reset*)
|
||||
(format t "~a~%" response))))))
|
||||
|
||||
(defun service-cmd (action id name ports bootstrap service-type)
|
||||
(defun service-cmd (action id name ports bootstrap bootstrap-file service-type input-files)
|
||||
(let ((api-key (get-api-key)))
|
||||
(cond
|
||||
((string= action "list")
|
||||
|
|
@ -269,8 +297,12 @@
|
|||
((and (string= action "create") name)
|
||||
(let* ((ports-json (if ports (format nil ",\"ports\":[~a]" ports) ""))
|
||||
(bootstrap-json (if bootstrap (format nil ",\"bootstrap\":\"~a\"" (escape-json bootstrap)) ""))
|
||||
(bootstrap-content-json (if bootstrap-file
|
||||
(format nil ",\"bootstrap_content\":\"~a\"" (escape-json (read-file bootstrap-file)))
|
||||
""))
|
||||
(type-json (if service-type (format nil ",\"service_type\":\"~a\"" service-type) ""))
|
||||
(json (format nil "{\"name\":\"~a\"~a~a~a}" name ports-json bootstrap-json type-json))
|
||||
(input-files-json (build-input-files-json input-files))
|
||||
(json (format nil "{\"name\":\"~a\"~a~a~a~a~a}" name ports-json bootstrap-json bootstrap-content-json type-json input-files-json))
|
||||
(response (curl-post api-key "/services" json)))
|
||||
(format t "~aService created~a~%" *green* *reset*)
|
||||
(format t "~a~%" response)))
|
||||
|
|
@ -364,6 +396,19 @@
|
|||
(defun key-cmd (extend-flag)
|
||||
(validate-key extend-flag))
|
||||
|
||||
(defun parse-input-files (args)
|
||||
"Parse -f flags from args and return list of filenames"
|
||||
(let ((files nil))
|
||||
(loop for i from 0 below (1- (length args))
|
||||
do (when (string= (nth i args) "-f")
|
||||
(let ((file (nth (1+ i) args)))
|
||||
(if (probe-file file)
|
||||
(push file files)
|
||||
(progn
|
||||
(format *error-output* "Error: File not found: ~a~%" file)
|
||||
(uiop:quit 1))))))
|
||||
(nreverse files)))
|
||||
|
||||
(defun main ()
|
||||
(let ((args (uiop:command-line-arguments)))
|
||||
(if (null args)
|
||||
|
|
@ -377,45 +422,57 @@
|
|||
((string= (first args) "session")
|
||||
(cond
|
||||
((and (> (length args) 1) (string= (second args) "--list"))
|
||||
(session-cmd "list" nil nil))
|
||||
(session-cmd "list" nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--kill"))
|
||||
(session-cmd "kill" (third args) nil))
|
||||
(session-cmd "kill" (third args) nil nil))
|
||||
(t
|
||||
(session-cmd "create" nil nil))))
|
||||
;; Parse session create options including -f
|
||||
(let* ((rest-args (cdr args))
|
||||
(shell nil)
|
||||
(input-files (parse-input-files rest-args)))
|
||||
(loop for i from 0 below (1- (length rest-args))
|
||||
do (let ((opt (nth i rest-args))
|
||||
(val (nth (1+ i) rest-args)))
|
||||
(cond
|
||||
((or (string= opt "--shell") (string= opt "-s")) (setf shell val)))))
|
||||
(session-cmd "create" nil shell input-files)))))
|
||||
((string= (first args) "service")
|
||||
(cond
|
||||
((and (> (length args) 1) (string= (second args) "--list"))
|
||||
(service-cmd "list" nil nil nil nil nil))
|
||||
(service-cmd "list" nil nil nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--info"))
|
||||
(service-cmd "info" (third args) nil nil nil nil))
|
||||
(service-cmd "info" (third args) nil nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--logs"))
|
||||
(service-cmd "logs" (third args) nil nil nil nil))
|
||||
(service-cmd "logs" (third args) nil nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--freeze"))
|
||||
(service-cmd "sleep" (third args) nil nil nil nil))
|
||||
(service-cmd "sleep" (third args) nil nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--unfreeze"))
|
||||
(service-cmd "wake" (third args) nil nil nil nil))
|
||||
(service-cmd "wake" (third args) nil nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--destroy"))
|
||||
(service-cmd "destroy" (third args) nil nil nil nil))
|
||||
(service-cmd "destroy" (third args) nil nil nil nil nil nil))
|
||||
((and (> (length args) 3) (string= (second args) "--execute"))
|
||||
(service-cmd "execute" (third args) nil nil (fourth args) nil))
|
||||
(service-cmd "execute" (third args) nil nil (fourth args) nil nil nil))
|
||||
((and (> (length args) 3) (string= (second args) "--dump-bootstrap"))
|
||||
(service-cmd "dump-bootstrap" (third args) nil nil nil (fourth args)))
|
||||
(service-cmd "dump-bootstrap" (third args) nil nil nil nil (fourth args) nil))
|
||||
((and (> (length args) 2) (string= (second args) "--dump-bootstrap"))
|
||||
(service-cmd "dump-bootstrap" (third args) nil nil nil nil))
|
||||
(service-cmd "dump-bootstrap" (third args) nil nil nil nil nil nil))
|
||||
((and (> (length args) 2) (string= (second args) "--name"))
|
||||
(let* ((name (third args))
|
||||
(rest-args (nthcdr 3 args))
|
||||
(ports nil)
|
||||
(bootstrap nil)
|
||||
(service-type nil))
|
||||
(loop for i from 0 below (length rest-args) by 2
|
||||
(bootstrap-file nil)
|
||||
(service-type nil)
|
||||
(input-files (parse-input-files rest-args)))
|
||||
(loop for i from 0 below (1- (length rest-args))
|
||||
do (let ((opt (nth i rest-args))
|
||||
(val (nth (1+ i) rest-args)))
|
||||
(cond
|
||||
((string= opt "--ports") (setf ports val))
|
||||
((string= opt "--bootstrap") (setf bootstrap val))
|
||||
((string= opt "--bootstrap-file") (setf bootstrap-file val))
|
||||
((string= opt "--type") (setf service-type val)))))
|
||||
(service-cmd "create" nil name ports bootstrap service-type)))
|
||||
(service-cmd "create" nil name ports bootstrap bootstrap-file service-type input-files)))
|
||||
(t
|
||||
(format t "Error: Invalid service command~%")
|
||||
(uiop:quit 1))))
|
||||
|
|
|
|||
47
un.lua
47
un.lua
|
|
@ -343,6 +343,19 @@ local function cmd_session(options)
|
|||
if options.screen then payload.persistence = "screen" end
|
||||
if options.audit then payload.audit = true end
|
||||
|
||||
-- Add input files
|
||||
if options.files and #options.files > 0 then
|
||||
local input_files = {}
|
||||
for _, filepath in ipairs(options.files) do
|
||||
local content = read_file(filepath)
|
||||
table.insert(input_files, {
|
||||
filename = filepath:match("([^/]+)$"),
|
||||
content_base64 = base64_encode(content)
|
||||
})
|
||||
end
|
||||
payload.input_files = input_files
|
||||
end
|
||||
|
||||
print(YELLOW .. "Creating session..." .. RESET)
|
||||
local result = api_request("/sessions", "POST", payload, keys)
|
||||
print(GREEN .. "Session created: " .. (result.id or "N/A") .. RESET)
|
||||
|
|
@ -584,13 +597,28 @@ local function cmd_service(options)
|
|||
payload.service_type = options.type
|
||||
end
|
||||
if options.bootstrap then
|
||||
local file = io.open(options.bootstrap, "r")
|
||||
if file then
|
||||
payload.bootstrap = file:read("*all")
|
||||
file:close()
|
||||
else
|
||||
payload.bootstrap = options.bootstrap
|
||||
payload.bootstrap = options.bootstrap
|
||||
end
|
||||
if options.bootstrap_file then
|
||||
local file = io.open(options.bootstrap_file, "r")
|
||||
if not file then
|
||||
io.stderr:write(RED .. "Error: Bootstrap file not found: " .. options.bootstrap_file .. RESET .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
payload.bootstrap_content = file:read("*all")
|
||||
file:close()
|
||||
end
|
||||
-- Add input files
|
||||
if options.files and #options.files > 0 then
|
||||
local input_files = {}
|
||||
for _, filepath in ipairs(options.files) do
|
||||
local content = read_file(filepath)
|
||||
table.insert(input_files, {
|
||||
filename = filepath:match("([^/]+)$"),
|
||||
content_base64 = base64_encode(content)
|
||||
})
|
||||
end
|
||||
payload.input_files = input_files
|
||||
end
|
||||
if options.network then payload.network = options.network end
|
||||
if options.vcpu then payload.vcpu = options.vcpu end
|
||||
|
|
@ -629,6 +657,7 @@ local function main()
|
|||
domains = nil,
|
||||
type = nil,
|
||||
bootstrap = nil,
|
||||
bootstrap_file = nil,
|
||||
info = nil,
|
||||
logs = nil,
|
||||
tail = nil,
|
||||
|
|
@ -700,6 +729,9 @@ local function main()
|
|||
elseif a == "--bootstrap" then
|
||||
i = i + 1
|
||||
options.bootstrap = arg[i]
|
||||
elseif a == "--bootstrap-file" then
|
||||
i = i + 1
|
||||
options.bootstrap_file = arg[i]
|
||||
elseif a == "--info" then
|
||||
i = i + 1
|
||||
options.info = arg[i]
|
||||
|
|
@ -780,7 +812,8 @@ Service options:
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
63
un.m
63
un.m
|
|
@ -450,6 +450,7 @@ void cmdSession(NSArray* args) {
|
|||
NSString* shell = nil;
|
||||
NSString* network = nil;
|
||||
int vcpu = 0;
|
||||
NSMutableArray* inputFiles = [NSMutableArray array];
|
||||
|
||||
// Parse arguments
|
||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||
|
|
@ -460,6 +461,8 @@ void cmdSession(NSArray* args) {
|
|||
killId = args[++i];
|
||||
} else if ([arg isEqualToString:@"--shell"] && i + 1 < [args count]) {
|
||||
shell = args[++i];
|
||||
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
||||
[inputFiles addObject:args[++i]];
|
||||
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
|
||||
network = args[++i];
|
||||
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
|
||||
|
|
@ -499,6 +502,26 @@ void cmdSession(NSArray* args) {
|
|||
if (network) payload[@"network"] = network;
|
||||
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
||||
|
||||
// Add input files
|
||||
if ([inputFiles count] > 0) {
|
||||
NSFileManager* fm = [NSFileManager defaultManager];
|
||||
NSMutableArray* files = [NSMutableArray array];
|
||||
for (NSString* filepath in inputFiles) {
|
||||
if (![fm fileExistsAtPath:filepath]) {
|
||||
fprintf(stderr, "%sError: Input file not found: %s%s\n",
|
||||
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
NSData* content = [NSData dataWithContentsOfFile:filepath];
|
||||
NSString* b64Content = [content base64EncodedStringWithOptions:0];
|
||||
[files addObject:@{
|
||||
@"filename": [filepath lastPathComponent],
|
||||
@"content_base64": b64Content
|
||||
}];
|
||||
}
|
||||
payload[@"input_files"] = files;
|
||||
}
|
||||
|
||||
printf("%sCreating session...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
|
||||
NSDictionary* result = apiRequest(@"/sessions", @"POST", payload, publicKey, secretKey);
|
||||
printf("%sSession created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
|
||||
|
|
@ -521,8 +544,10 @@ void cmdService(NSArray* args) {
|
|||
NSString* ports = nil;
|
||||
NSString* type = nil;
|
||||
NSString* bootstrap = nil;
|
||||
NSString* bootstrapFile = nil;
|
||||
NSString* network = nil;
|
||||
int vcpu = 0;
|
||||
NSMutableArray* inputFiles = [NSMutableArray array];
|
||||
|
||||
// Parse arguments
|
||||
for (NSUInteger i = 0; i < [args count]; i++) {
|
||||
|
|
@ -551,6 +576,10 @@ void cmdService(NSArray* args) {
|
|||
type = args[++i];
|
||||
} else if ([arg isEqualToString:@"--bootstrap"] && i + 1 < [args count]) {
|
||||
bootstrap = args[++i];
|
||||
} else if ([arg isEqualToString:@"--bootstrap-file"] && i + 1 < [args count]) {
|
||||
bootstrapFile = args[++i];
|
||||
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
||||
[inputFiles addObject:args[++i]];
|
||||
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
|
||||
network = args[++i];
|
||||
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
|
||||
|
|
@ -669,15 +698,41 @@ void cmdService(NSArray* args) {
|
|||
}
|
||||
|
||||
if (bootstrap) {
|
||||
payload[@"bootstrap"] = bootstrap;
|
||||
}
|
||||
|
||||
if (bootstrapFile) {
|
||||
NSFileManager* fm = [NSFileManager defaultManager];
|
||||
if ([fm fileExistsAtPath:bootstrap]) {
|
||||
NSString* content = [NSString stringWithContentsOfFile:bootstrap encoding:NSUTF8StringEncoding error:nil];
|
||||
payload[@"bootstrap"] = content;
|
||||
if ([fm fileExistsAtPath:bootstrapFile]) {
|
||||
NSString* content = [NSString stringWithContentsOfFile:bootstrapFile encoding:NSUTF8StringEncoding error:nil];
|
||||
payload[@"bootstrap_content"] = content;
|
||||
} else {
|
||||
payload[@"bootstrap"] = bootstrap;
|
||||
fprintf(stderr, "%sError: Bootstrap file not found: %s%s\n",
|
||||
[RED UTF8String], [bootstrapFile UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add input files
|
||||
if ([inputFiles count] > 0) {
|
||||
NSFileManager* fm = [NSFileManager defaultManager];
|
||||
NSMutableArray* files = [NSMutableArray array];
|
||||
for (NSString* filepath in inputFiles) {
|
||||
if (![fm fileExistsAtPath:filepath]) {
|
||||
fprintf(stderr, "%sError: Input file not found: %s%s\n",
|
||||
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
NSData* content = [NSData dataWithContentsOfFile:filepath];
|
||||
NSString* b64Content = [content base64EncodedStringWithOptions:0];
|
||||
[files addObject:@{
|
||||
@"filename": [filepath lastPathComponent],
|
||||
@"content_base64": b64Content
|
||||
}];
|
||||
}
|
||||
payload[@"input_files"] = files;
|
||||
}
|
||||
|
||||
if (network) payload[@"network"] = network;
|
||||
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
||||
|
||||
|
|
|
|||
92
un.ml
92
un.ml
|
|
@ -91,6 +91,25 @@ let read_file filename =
|
|||
close_in ic;
|
||||
s
|
||||
|
||||
(* Base64 encode a file using shell command *)
|
||||
let base64_encode_file filename =
|
||||
let cmd = Printf.sprintf "base64 -w0 %s" (Filename.quote filename) in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let result = try input_line ic with End_of_file -> "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
String.trim result
|
||||
|
||||
(* Build input_files JSON from list of filenames *)
|
||||
let build_input_files_json files =
|
||||
if files = [] then ""
|
||||
else
|
||||
let entries = List.map (fun f ->
|
||||
let basename = Filename.basename f in
|
||||
let content = base64_encode_file f in
|
||||
Printf.sprintf "{\"filename\":\"%s\",\"content\":\"%s\"}" basename content
|
||||
) files in
|
||||
",\"input_files\":[" ^ (String.concat "," entries) ^ "]"
|
||||
|
||||
(* Get file extension *)
|
||||
let get_extension filename =
|
||||
try
|
||||
|
|
@ -407,7 +426,7 @@ let key_command extend =
|
|||
validate_key api_key extend
|
||||
|
||||
(* Session command *)
|
||||
let session_command action shell network vcpu =
|
||||
let session_command action shell network vcpu input_files =
|
||||
let api_key = get_api_key () in
|
||||
match action with
|
||||
| "list" ->
|
||||
|
|
@ -425,7 +444,8 @@ let session_command action shell network vcpu =
|
|||
let sh = match shell with Some s -> s | None -> "bash" in
|
||||
let network_json = match network with Some n -> Printf.sprintf ",\"network\":\"%s\"" n | None -> "" in
|
||||
let vcpu_json = match vcpu with Some v -> Printf.sprintf ",\"vcpu\":%d" v | None -> "" in
|
||||
let json = Printf.sprintf "{\"shell\":\"%s\"%s%s}" sh network_json vcpu_json in
|
||||
let input_files_json = build_input_files_json input_files in
|
||||
let json = Printf.sprintf "{\"shell\":\"%s\"%s%s%s}" sh network_json vcpu_json input_files_json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
|
|
@ -445,7 +465,7 @@ let session_command action shell network vcpu =
|
|||
| _ -> ()
|
||||
|
||||
(* Service command *)
|
||||
let service_command action name ports bootstrap service_type network vcpu =
|
||||
let service_command action name ports bootstrap bootstrap_file service_type network vcpu input_files =
|
||||
let api_key = get_api_key () in
|
||||
match action with
|
||||
| "list" ->
|
||||
|
|
@ -576,10 +596,17 @@ let service_command action name ports bootstrap service_type network vcpu =
|
|||
| Some n ->
|
||||
let ports_json = match ports with Some p -> Printf.sprintf ",\"ports\":[%s]" p | None -> "" in
|
||||
let bootstrap_json = match bootstrap with Some b -> Printf.sprintf ",\"bootstrap\":\"%s\"" (escape_json b) | None -> "" in
|
||||
let bootstrap_content_json = match bootstrap_file with
|
||||
| Some f ->
|
||||
let content = read_file f in
|
||||
Printf.sprintf ",\"bootstrap_content\":\"%s\"" (escape_json content)
|
||||
| None -> ""
|
||||
in
|
||||
let service_type_json = match service_type with Some t -> Printf.sprintf ",\"service_type\":\"%s\"" t | None -> "" in
|
||||
let network_json = match network with Some net -> Printf.sprintf ",\"network\":\"%s\"" net | None -> "" in
|
||||
let vcpu_json = match vcpu with Some v -> Printf.sprintf ",\"vcpu\":%d" v | None -> "" in
|
||||
let json = Printf.sprintf "{\"name\":\"%s\"%s%s%s%s%s}" n ports_json bootstrap_json service_type_json network_json vcpu_json in
|
||||
let input_files_json = build_input_files_json input_files in
|
||||
let json = Printf.sprintf "{\"name\":\"%s\"%s%s%s%s%s%s%s}" n ports_json bootstrap_json bootstrap_content_json service_type_json network_json vcpu_json input_files_json in
|
||||
let tmp_file = Printf.sprintf "/tmp/un_ocaml_%d.json" (Random.int 999999) in
|
||||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
|
|
@ -601,6 +628,18 @@ let service_command action name ports bootstrap service_type network vcpu =
|
|||
exit 1)
|
||||
| _ -> ()
|
||||
|
||||
(* Parse -f flags from argument list *)
|
||||
let rec parse_input_files acc = function
|
||||
| [] -> List.rev acc
|
||||
| "-f" :: file :: rest ->
|
||||
if Sys.file_exists file then
|
||||
parse_input_files (file :: acc) rest
|
||||
else begin
|
||||
Printf.fprintf stderr "Error: File not found: %s\n" file;
|
||||
exit 1
|
||||
end
|
||||
| _ :: rest -> parse_input_files acc rest
|
||||
|
||||
(* Parse arguments *)
|
||||
let () =
|
||||
Random.self_init ();
|
||||
|
|
@ -616,37 +655,42 @@ let () =
|
|||
let extend = List.mem "--extend" rest in
|
||||
key_command extend
|
||||
| "session" :: rest ->
|
||||
let input_files = parse_input_files [] rest in
|
||||
let rec parse_session action shell network vcpu = function
|
||||
| [] -> session_command action shell network vcpu
|
||||
| [] -> session_command action shell network vcpu input_files
|
||||
| "--list" :: rest -> parse_session "list" shell network vcpu rest
|
||||
| "--kill" :: id :: rest -> parse_session "kill" (Some id) network vcpu rest
|
||||
| "--shell" :: sh :: rest | "-s" :: sh :: rest -> parse_session action (Some sh) network vcpu rest
|
||||
| "-n" :: net :: rest -> parse_session action shell (Some net) vcpu rest
|
||||
| "-v" :: v :: rest -> parse_session action shell network (Some (int_of_string v)) rest
|
||||
| "-f" :: _ :: rest -> parse_session action shell network vcpu rest (* skip -f, already parsed *)
|
||||
| _ :: rest -> parse_session action shell network vcpu rest
|
||||
in
|
||||
parse_session "create" None None None rest
|
||||
| "service" :: rest ->
|
||||
let rec parse_service action name ports bootstrap service_type network vcpu = function
|
||||
| [] -> service_command action name ports bootstrap service_type network vcpu
|
||||
| "--list" :: rest -> parse_service "list" name ports bootstrap service_type network vcpu rest
|
||||
| "--info" :: id :: rest -> parse_service "info" (Some id) ports bootstrap service_type network vcpu rest
|
||||
| "--logs" :: id :: rest -> parse_service "logs" (Some id) ports bootstrap service_type network vcpu rest
|
||||
| "--freeze" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap service_type network vcpu rest
|
||||
| "--unfreeze" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap service_type network vcpu rest
|
||||
| "--destroy" :: id :: rest -> parse_service "destroy" (Some id) ports bootstrap service_type network vcpu rest
|
||||
| "--execute" :: id :: "--command" :: cmd :: rest -> parse_service "execute" (Some id) ports (Some cmd) service_type network vcpu rest
|
||||
| "--dump-bootstrap" :: id :: file :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap (Some file) network vcpu rest
|
||||
| "--dump-bootstrap" :: id :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap service_type network vcpu rest
|
||||
| "--name" :: n :: rest -> parse_service "create" (Some n) ports bootstrap service_type network vcpu rest
|
||||
| "--ports" :: p :: rest -> parse_service action name (Some p) bootstrap service_type network vcpu rest
|
||||
| "--bootstrap" :: b :: rest -> parse_service action name ports (Some b) service_type network vcpu rest
|
||||
| "--type" :: t :: rest -> parse_service action name ports bootstrap (Some t) network vcpu rest
|
||||
| "-n" :: net :: rest -> parse_service action name ports bootstrap service_type (Some net) vcpu rest
|
||||
| "-v" :: v :: rest -> parse_service action name ports bootstrap service_type network (Some (int_of_string v)) rest
|
||||
| _ :: rest -> parse_service action name ports bootstrap service_type network vcpu rest
|
||||
let input_files = parse_input_files [] rest in
|
||||
let rec parse_service action name ports bootstrap bootstrap_file service_type network vcpu = function
|
||||
| [] -> service_command action name ports bootstrap bootstrap_file service_type network vcpu input_files
|
||||
| "--list" :: rest -> parse_service "list" name ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--info" :: id :: rest -> parse_service "info" (Some id) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--logs" :: id :: rest -> parse_service "logs" (Some id) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--freeze" :: id :: rest -> parse_service "sleep" (Some id) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--unfreeze" :: id :: rest -> parse_service "wake" (Some id) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--destroy" :: id :: rest -> parse_service "destroy" (Some id) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--execute" :: id :: "--command" :: cmd :: rest -> parse_service "execute" (Some id) ports (Some cmd) bootstrap_file service_type network vcpu rest
|
||||
| "--dump-bootstrap" :: id :: file :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap (Some file) service_type network vcpu rest
|
||||
| "--dump-bootstrap" :: id :: rest -> parse_service "dump_bootstrap" (Some id) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--name" :: n :: rest -> parse_service "create" (Some n) ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--ports" :: p :: rest -> parse_service action name (Some p) bootstrap bootstrap_file service_type network vcpu rest
|
||||
| "--bootstrap" :: b :: rest -> parse_service action name ports (Some b) bootstrap_file service_type network vcpu rest
|
||||
| "--bootstrap-file" :: f :: rest -> parse_service action name ports bootstrap (Some f) service_type network vcpu rest
|
||||
| "--type" :: t :: rest -> parse_service action name ports bootstrap bootstrap_file (Some t) network vcpu rest
|
||||
| "-n" :: net :: rest -> parse_service action name ports bootstrap bootstrap_file service_type (Some net) vcpu rest
|
||||
| "-v" :: v :: rest -> parse_service action name ports bootstrap bootstrap_file service_type network (Some (int_of_string v)) rest
|
||||
| "-f" :: _ :: rest -> parse_service action name ports bootstrap bootstrap_file service_type network vcpu rest (* skip -f, already parsed *)
|
||||
| _ :: rest -> parse_service action name ports bootstrap bootstrap_file service_type network vcpu rest
|
||||
in
|
||||
parse_service "create" None None None None None None rest
|
||||
parse_service "create" None None None None None None None rest
|
||||
| args ->
|
||||
let rec parse_execute file env_vars artifacts out_dir network vcpu = function
|
||||
| [] -> execute_command file env_vars artifacts out_dir network vcpu
|
||||
|
|
|
|||
58
un.nim
58
un.nim
|
|
@ -76,6 +76,20 @@ proc escapeJson(s: string): string =
|
|||
of '\t': result.add("\\t")
|
||||
else: result.add(c)
|
||||
|
||||
proc base64EncodeFile(filename: string): string =
|
||||
let cmd = fmt"base64 -w0 '{filename}'"
|
||||
result = execProcess(cmd).strip()
|
||||
|
||||
proc buildInputFilesJson(files: seq[string]): string =
|
||||
if files.len == 0:
|
||||
return ""
|
||||
var entries: seq[string] = @[]
|
||||
for f in files:
|
||||
let basename = extractFilename(f)
|
||||
let content = base64EncodeFile(f)
|
||||
entries.add(fmt"""{{"filename":"{basename}","content":"{content}"}}""")
|
||||
result = fmt""","input_files":[{entries.join(",")}]"""
|
||||
|
||||
proc computeHmac(secretKey: string, message: string): string =
|
||||
let cmd = fmt"echo -n '{message}' | openssl dgst -sha256 -hmac '{secretKey}' -hex 2>/dev/null | sed 's/.*= //'"
|
||||
result = execProcess(cmd).strip()
|
||||
|
|
@ -136,7 +150,7 @@ proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network:
|
|||
let cmd = fmt"""curl -s -X POST '{API_BASE}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
echo execCurl(cmd)
|
||||
|
||||
proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, screen: bool, publicKey: string, secretKey: string) =
|
||||
proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, screen: bool, inputFiles: seq[string], publicKey: string, secretKey: string) =
|
||||
if list:
|
||||
let authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions' {authHeaders}"""
|
||||
|
|
@ -156,6 +170,7 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
|
|||
if vcpu > 0: json.add(fmt""","vcpu":{vcpu}""")
|
||||
if tmux: json.add(""","persistence":"tmux"""")
|
||||
if screen: json.add(""","persistence":"screen"""")
|
||||
json.add(buildInputFilesJson(inputFiles))
|
||||
json.add("}")
|
||||
|
||||
echo YELLOW & "Creating session..." & RESET
|
||||
|
|
@ -163,7 +178,7 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
|
|||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
echo execCurl(cmd)
|
||||
|
||||
proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, publicKey: string, secretKey: string) =
|
||||
proc cmdService(name, ports, bootstrap, bootstrapFile, serviceType: string, list: bool, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network: string, vcpu: int, inputFiles: seq[string], publicKey: string, secretKey: string) =
|
||||
if list:
|
||||
let authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services' {authHeaders}"""
|
||||
|
|
@ -294,14 +309,18 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
|
|||
var json = fmt"""{"name":"{name}""""
|
||||
if ports != "": json.add(fmt""","ports":[{ports}]""")
|
||||
if bootstrap != "":
|
||||
if fileExists(bootstrap):
|
||||
let bootCode = readFile(bootstrap)
|
||||
json.add(fmt""","bootstrap":"{escapeJson(bootCode)}"""")
|
||||
json.add(fmt""","bootstrap":"{escapeJson(bootstrap)}"""")
|
||||
if bootstrapFile != "":
|
||||
if fileExists(bootstrapFile):
|
||||
let bootCode = readFile(bootstrapFile)
|
||||
json.add(fmt""","bootstrap_content":"{escapeJson(bootCode)}"""")
|
||||
else:
|
||||
json.add(fmt""","bootstrap":"{escapeJson(bootstrap)}"""")
|
||||
stderr.writeLine(RED & "Error: Bootstrap file not found: " & bootstrapFile & RESET)
|
||||
quit(1)
|
||||
if serviceType != "": json.add(fmt""","service_type":"{serviceType}"""")
|
||||
if network != "": json.add(fmt""","network":"{network}"""")
|
||||
if vcpu > 0: json.add(fmt""","vcpu":{vcpu}""")
|
||||
json.add(buildInputFilesJson(inputFiles))
|
||||
json.add("}")
|
||||
|
||||
echo YELLOW & "Creating service..." & RESET
|
||||
|
|
@ -422,6 +441,7 @@ proc main() =
|
|||
var kill, shell, network = ""
|
||||
var vcpu = 0
|
||||
var tmux, screen = false
|
||||
var inputFiles: seq[string] = @[]
|
||||
var i = 1
|
||||
while i < args.len:
|
||||
case args[i]
|
||||
|
|
@ -433,21 +453,32 @@ proc main() =
|
|||
of "--tmux": tmux = true
|
||||
of "--screen": screen = true
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
of "-f":
|
||||
let file = args[i+1]
|
||||
if fileExists(file):
|
||||
inputFiles.add(file)
|
||||
else:
|
||||
stderr.writeLine("Error: File not found: " & file)
|
||||
quit(1)
|
||||
inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey)
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, inputFiles, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "service":
|
||||
var name, ports, bootstrap, serviceType = ""
|
||||
var name, ports, bootstrap, bootstrapFile, serviceType = ""
|
||||
var list = false
|
||||
var info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network = ""
|
||||
var vcpu = 0
|
||||
var inputFiles: seq[string] = @[]
|
||||
var i = 1
|
||||
while i < args.len:
|
||||
case args[i]
|
||||
of "--name": name = args[i+1]; inc i
|
||||
of "--ports": ports = args[i+1]; inc i
|
||||
of "--bootstrap": bootstrap = args[i+1]; inc i
|
||||
of "--bootstrap-file": bootstrapFile = args[i+1]; inc i
|
||||
of "--type": serviceType = args[i+1]; inc i
|
||||
of "--list": list = true
|
||||
of "--info": info = args[i+1]; inc i
|
||||
|
|
@ -463,8 +494,17 @@ proc main() =
|
|||
of "-n": network = args[i+1]; inc i
|
||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
of "-f":
|
||||
let file = args[i+1]
|
||||
if fileExists(file):
|
||||
inputFiles.add(file)
|
||||
else:
|
||||
stderr.writeLine("Error: File not found: " & file)
|
||||
quit(1)
|
||||
inc i
|
||||
else: discard
|
||||
inc i
|
||||
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey)
|
||||
cmdService(name, ports, bootstrap, bootstrapFile, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, inputFiles, publicKey, secretKey)
|
||||
return
|
||||
|
||||
# Execute mode
|
||||
|
|
|
|||
49
un.php
49
un.php
|
|
@ -288,6 +288,22 @@ function cmd_session($options) {
|
|||
if ($options['screen']) $payload['persistence'] = 'screen';
|
||||
if ($options['audit']) $payload['audit'] = true;
|
||||
|
||||
// Add input files
|
||||
if (!empty($options['files'])) {
|
||||
$input_files = [];
|
||||
foreach ($options['files'] as $filepath) {
|
||||
if (!file_exists($filepath)) {
|
||||
fwrite(STDERR, RED . "Error: Input file not found: $filepath" . RESET . "\n");
|
||||
exit(1);
|
||||
}
|
||||
$input_files[] = [
|
||||
'filename' => basename($filepath),
|
||||
'content_base64' => base64_encode(file_get_contents($filepath))
|
||||
];
|
||||
}
|
||||
$payload['input_files'] = $input_files;
|
||||
}
|
||||
|
||||
echo YELLOW . "Creating session..." . RESET . "\n";
|
||||
$result = api_request('/sessions', 'POST', $payload, $keys);
|
||||
echo GREEN . "Session created: " . ($result['id'] ?? 'N/A') . RESET . "\n";
|
||||
|
|
@ -524,11 +540,29 @@ function cmd_service($options) {
|
|||
$payload['service_type'] = $options['type'];
|
||||
}
|
||||
if ($options['bootstrap']) {
|
||||
if (file_exists($options['bootstrap'])) {
|
||||
$payload['bootstrap'] = file_get_contents($options['bootstrap']);
|
||||
} else {
|
||||
$payload['bootstrap'] = $options['bootstrap'];
|
||||
$payload['bootstrap'] = $options['bootstrap'];
|
||||
}
|
||||
if ($options['bootstrap_file']) {
|
||||
if (!file_exists($options['bootstrap_file'])) {
|
||||
fwrite(STDERR, RED . "Error: Bootstrap file not found: {$options['bootstrap_file']}" . RESET . "\n");
|
||||
exit(1);
|
||||
}
|
||||
$payload['bootstrap_content'] = file_get_contents($options['bootstrap_file']);
|
||||
}
|
||||
// Add input files
|
||||
if (!empty($options['files'])) {
|
||||
$input_files = [];
|
||||
foreach ($options['files'] as $filepath) {
|
||||
if (!file_exists($filepath)) {
|
||||
fwrite(STDERR, RED . "Error: Input file not found: $filepath" . RESET . "\n");
|
||||
exit(1);
|
||||
}
|
||||
$input_files[] = [
|
||||
'filename' => basename($filepath),
|
||||
'content_base64' => base64_encode(file_get_contents($filepath))
|
||||
];
|
||||
}
|
||||
$payload['input_files'] = $input_files;
|
||||
}
|
||||
if ($options['network']) $payload['network'] = $options['network'];
|
||||
if ($options['vcpu']) $payload['vcpu'] = $options['vcpu'];
|
||||
|
|
@ -569,6 +603,7 @@ function main() {
|
|||
'domains' => null,
|
||||
'type' => null,
|
||||
'bootstrap' => null,
|
||||
'bootstrap_file' => null,
|
||||
'info' => null,
|
||||
'logs' => null,
|
||||
'tail' => null,
|
||||
|
|
@ -650,6 +685,9 @@ function main() {
|
|||
case '--bootstrap':
|
||||
$options['bootstrap'] = $argv[++$i];
|
||||
break;
|
||||
case '--bootstrap-file':
|
||||
$options['bootstrap_file'] = $argv[++$i];
|
||||
break;
|
||||
case '--info':
|
||||
$options['info'] = $argv[++$i];
|
||||
break;
|
||||
|
|
@ -731,7 +769,8 @@ Service options:
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
62
un.pl
62
un.pl
|
|
@ -281,6 +281,26 @@ sub cmd_session {
|
|||
$payload->{persistence} = 'screen' if $options->{screen};
|
||||
$payload->{audit} = JSON::PP::true if $options->{audit};
|
||||
|
||||
# Add input files
|
||||
if ($options->{files} && @{$options->{files}}) {
|
||||
my @input_files;
|
||||
foreach my $filepath (@{$options->{files}}) {
|
||||
unless (-e $filepath) {
|
||||
print STDERR "${RED}Error: Input file not found: $filepath${RESET}\n";
|
||||
exit 1;
|
||||
}
|
||||
open my $f, '<:raw', $filepath or die "Cannot read file: $!";
|
||||
local $/;
|
||||
my $content = <$f>;
|
||||
close $f;
|
||||
push @input_files, {
|
||||
filename => basename($filepath),
|
||||
content_base64 => encode_base64($content, '')
|
||||
};
|
||||
}
|
||||
$payload->{input_files} = \@input_files;
|
||||
}
|
||||
|
||||
print "${YELLOW}Creating session...${RESET}\n";
|
||||
my $result = api_request('/sessions', 'POST', $payload, $public_key, $secret_key);
|
||||
print "${GREEN}Session created: ", ($result->{id} // 'N/A'), "${RESET}\n";
|
||||
|
|
@ -396,14 +416,36 @@ sub cmd_service {
|
|||
$payload->{service_type} = $options->{type};
|
||||
}
|
||||
if ($options->{bootstrap}) {
|
||||
if (-e $options->{bootstrap}) {
|
||||
open my $fh, '<', $options->{bootstrap} or die "Cannot read file: $!";
|
||||
local $/;
|
||||
$payload->{bootstrap} = <$fh>;
|
||||
close $fh;
|
||||
} else {
|
||||
$payload->{bootstrap} = $options->{bootstrap};
|
||||
$payload->{bootstrap} = $options->{bootstrap};
|
||||
}
|
||||
if ($options->{bootstrap_file}) {
|
||||
if (! -e $options->{bootstrap_file}) {
|
||||
print STDERR "${RED}Error: Bootstrap file not found: $options->{bootstrap_file}${RESET}\n";
|
||||
exit 1;
|
||||
}
|
||||
open my $fh, '<', $options->{bootstrap_file} or die "Cannot read file: $!";
|
||||
local $/;
|
||||
$payload->{bootstrap_content} = <$fh>;
|
||||
close $fh;
|
||||
}
|
||||
# Add input files
|
||||
if ($options->{files} && @{$options->{files}}) {
|
||||
my @input_files;
|
||||
foreach my $filepath (@{$options->{files}}) {
|
||||
unless (-e $filepath) {
|
||||
print STDERR "${RED}Error: Input file not found: $filepath${RESET}\n";
|
||||
exit 1;
|
||||
}
|
||||
open my $f, '<:raw', $filepath or die "Cannot read file: $!";
|
||||
local $/;
|
||||
my $content = <$f>;
|
||||
close $f;
|
||||
push @input_files, {
|
||||
filename => basename($filepath),
|
||||
content_base64 => encode_base64($content, '')
|
||||
};
|
||||
}
|
||||
$payload->{input_files} = \@input_files;
|
||||
}
|
||||
$payload->{network} = $options->{network} if $options->{network};
|
||||
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
|
||||
|
|
@ -520,6 +562,7 @@ sub main {
|
|||
domains => undef,
|
||||
type => undef,
|
||||
bootstrap => undef,
|
||||
bootstrap_file => undef,
|
||||
info => undef,
|
||||
logs => undef,
|
||||
tail => undef,
|
||||
|
|
@ -576,6 +619,8 @@ sub main {
|
|||
$options{type} = $ARGV[++$i];
|
||||
} elsif ($arg eq '--bootstrap') {
|
||||
$options{bootstrap} = $ARGV[++$i];
|
||||
} elsif ($arg eq '--bootstrap-file') {
|
||||
$options{bootstrap_file} = $ARGV[++$i];
|
||||
} elsif ($arg eq '--info') {
|
||||
$options{info} = $ARGV[++$i];
|
||||
} elsif ($arg eq '--logs') {
|
||||
|
|
@ -644,7 +689,8 @@ Service options:
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
126
un.pro
126
un.pro
|
|
@ -150,6 +150,32 @@ session_kill(SessionId) :-
|
|||
[SessionId, SecretKey, SessionId, PublicKey, SessionId]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Session create with optional input files
|
||||
session_create(Shell, InputFiles) :-
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
( Shell \= ''
|
||||
-> ShellVal = Shell
|
||||
; ShellVal = 'bash'
|
||||
),
|
||||
% Build file arguments for bash script
|
||||
build_file_args(InputFiles, FileArgs),
|
||||
format(atom(Cmd),
|
||||
'echo -e "\\x1b[33mCreating session...\\x1b[0m"; SHELL_VAL="~w"; INPUT_FILES=""; ~w if [ -n "$INPUT_FILES" ]; then BODY="{\\\"shell\\\":\\\"$SHELL_VAL\\\",\\\"input_files\\\":[$INPUT_FILES]}"; else BODY="{\\\"shell\\\":\\\"$SHELL_VAL\\\"}"; fi; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/sessions:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/sessions -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" | jq .',
|
||||
[ShellVal, FileArgs, SecretKey, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Build bash commands to base64 encode files
|
||||
build_file_args([], '').
|
||||
build_file_args(Files, Args) :-
|
||||
Files \= [],
|
||||
maplist(build_single_file_arg, Files, ArgList),
|
||||
atomic_list_concat(ArgList, ' ', Args).
|
||||
|
||||
build_single_file_arg(FilePath, Arg) :-
|
||||
file_base_name(FilePath, Basename),
|
||||
format(atom(Arg), 'CONTENT=$(base64 -w0 "~w"); if [ -z "$INPUT_FILES" ]; then INPUT_FILES="{\\\"filename\\\":\\\"~w\\\",\\\"content\\\":\\\"$CONTENT\\\"}"; else INPUT_FILES="$INPUT_FILES,{\\\"filename\\\":\\\"~w\\\",\\\"content\\\":\\\"$CONTENT\\\"}"; fi;', [FilePath, Basename, Basename]).
|
||||
|
||||
% Service list
|
||||
service_list :-
|
||||
get_public_key(PublicKey),
|
||||
|
|
@ -220,8 +246,8 @@ service_dump_bootstrap(ServiceId, DumpFile) :-
|
|||
),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service create
|
||||
service_create(Name, Ports, Bootstrap, ServiceType) :-
|
||||
% Service create with optional input files
|
||||
service_create(Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles) :-
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
% Build JSON payload
|
||||
|
|
@ -233,15 +259,24 @@ service_create(Name, Ports, Bootstrap, ServiceType) :-
|
|||
-> format(atom(BootstrapJson), ',"bootstrap":"~w"', [Bootstrap])
|
||||
; BootstrapJson = ''
|
||||
),
|
||||
( BootstrapFile \= ''
|
||||
-> ( exists_file(BootstrapFile)
|
||||
-> read_file_content(BootstrapFile, BootstrapContent),
|
||||
format(atom(BootstrapContentJson), ',"bootstrap_content":"~w"', [BootstrapContent])
|
||||
; format(user_error, 'Error: Bootstrap file not found: ~w~n', [BootstrapFile]),
|
||||
halt(1)
|
||||
)
|
||||
; BootstrapContentJson = ''
|
||||
),
|
||||
( ServiceType \= ''
|
||||
-> format(atom(ServiceTypeJson), ',"service_type":"~w"', [ServiceType])
|
||||
; ServiceTypeJson = ''
|
||||
),
|
||||
format(atom(Json), '{"name":"~w"~w~w~w}', [Name, PortsJson, BootstrapJson, ServiceTypeJson]),
|
||||
% Execute curl command with HMAC
|
||||
% Build file arguments for bash script
|
||||
build_file_args(InputFiles, FileArgs),
|
||||
format(atom(Cmd),
|
||||
'BODY=\'~w\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" && echo -e "\\x1b[32mService created\\x1b[0m"',
|
||||
[Json, SecretKey, PublicKey]),
|
||||
'echo -e "\\x1b[33mCreating service...\\x1b[0m"; INPUT_FILES=""; ~w if [ -n "$INPUT_FILES" ]; then INPUT_FILES_JSON=",\\\"input_files\\\":[$INPUT_FILES]"; else INPUT_FILES_JSON=""; fi; BODY="{\\\"name\\\":\\\"~w\\\"~w~w~w~w$INPUT_FILES_JSON}"; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" | jq . && echo -e "\\x1b[32mService created\\x1b[0m"',
|
||||
[FileArgs, Name, PortsJson, BootstrapJson, BootstrapContentJson, ServiceTypeJson, SecretKey, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Key validate
|
||||
|
|
@ -269,53 +304,78 @@ handle_key(_) :- validate_key(false).
|
|||
handle_session(['--list'|_]) :- session_list.
|
||||
handle_session(['-l'|_]) :- session_list.
|
||||
handle_session(['--kill', SessionId|_]) :- session_kill(SessionId).
|
||||
handle_session(_) :-
|
||||
write(user_error, 'Error: Use --list or --kill ID\n'),
|
||||
halt(1).
|
||||
handle_session(Args) :-
|
||||
parse_session_args(Args, '', [], Shell, InputFiles),
|
||||
session_create(Shell, InputFiles).
|
||||
|
||||
% Parse session arguments for -f and --shell
|
||||
parse_session_args([], Shell, Files, Shell, Files).
|
||||
parse_session_args(['--shell', ShellVal|Rest], _, Files, Shell, InputFiles) :-
|
||||
parse_session_args(Rest, ShellVal, Files, Shell, InputFiles).
|
||||
parse_session_args(['-s', ShellVal|Rest], _, Files, Shell, InputFiles) :-
|
||||
parse_session_args(Rest, ShellVal, Files, Shell, InputFiles).
|
||||
parse_session_args(['-f', FilePath|Rest], Shell, Files, ShellOut, InputFiles) :-
|
||||
( exists_file(FilePath)
|
||||
-> append(Files, [FilePath], NewFiles),
|
||||
parse_session_args(Rest, Shell, NewFiles, ShellOut, InputFiles)
|
||||
; format(user_error, 'Error: File not found: ~w~n', [FilePath]),
|
||||
halt(1)
|
||||
).
|
||||
parse_session_args([_|Rest], Shell, Files, ShellOut, InputFiles) :-
|
||||
parse_session_args(Rest, Shell, Files, ShellOut, InputFiles).
|
||||
|
||||
% Handle service subcommand
|
||||
handle_service(Args) :-
|
||||
parse_service_args(Args, '', '', '', '', Action),
|
||||
execute_service_action(Action).
|
||||
parse_service_args(Args, '', '', '', '', '', [], Action, InputFiles),
|
||||
execute_service_action(Action, InputFiles).
|
||||
|
||||
% Parse service arguments
|
||||
parse_service_args([], Name, Ports, Bootstrap, ServiceType, create) :-
|
||||
parse_service_args([], Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles, create, InputFiles) :-
|
||||
( Name \= ''
|
||||
-> service_create(Name, Ports, Bootstrap, ServiceType)
|
||||
-> service_create(Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles)
|
||||
; write(user_error, 'Error: --name required for service creation\n'),
|
||||
halt(1)
|
||||
).
|
||||
parse_service_args([], _, _, _, _, Action) :-
|
||||
parse_service_args([], _, _, _, _, _, InputFiles, Action, InputFiles) :-
|
||||
( Action = list
|
||||
-> service_list
|
||||
; write(user_error, 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --name\n'),
|
||||
halt(1)
|
||||
).
|
||||
parse_service_args(['--list'|_], _, _, _, _, _) :- service_list.
|
||||
parse_service_args(['-l'|_], _, _, _, _, _) :- service_list.
|
||||
parse_service_args(['--info', ServiceId|_], _, _, _, _, _) :- service_info(ServiceId).
|
||||
parse_service_args(['--logs', ServiceId|_], _, _, _, _, _) :- service_logs(ServiceId).
|
||||
parse_service_args(['--freeze', ServiceId|_], _, _, _, _, _) :- service_sleep(ServiceId).
|
||||
parse_service_args(['--unfreeze', ServiceId|_], _, _, _, _, _) :- service_wake(ServiceId).
|
||||
parse_service_args(['--destroy', ServiceId|_], _, _, _, _, _) :- service_destroy(ServiceId).
|
||||
parse_service_args(['--dump-bootstrap', ServiceId|Rest], _, _, _, _, _) :-
|
||||
parse_service_args(['--list'|_], _, _, _, _, _, _, _, _) :- service_list.
|
||||
parse_service_args(['-l'|_], _, _, _, _, _, _, _, _) :- service_list.
|
||||
parse_service_args(['--info', ServiceId|_], _, _, _, _, _, _, _, _) :- service_info(ServiceId).
|
||||
parse_service_args(['--logs', ServiceId|_], _, _, _, _, _, _, _, _) :- service_logs(ServiceId).
|
||||
parse_service_args(['--freeze', ServiceId|_], _, _, _, _, _, _, _, _) :- service_sleep(ServiceId).
|
||||
parse_service_args(['--unfreeze', ServiceId|_], _, _, _, _, _, _, _, _) :- service_wake(ServiceId).
|
||||
parse_service_args(['--destroy', ServiceId|_], _, _, _, _, _, _, _, _) :- service_destroy(ServiceId).
|
||||
parse_service_args(['--dump-bootstrap', ServiceId|Rest], _, _, _, _, _, _, _, _) :-
|
||||
( Rest = ['--dump-file', DumpFile|_]
|
||||
-> service_dump_bootstrap(ServiceId, DumpFile)
|
||||
; service_dump_bootstrap(ServiceId, '')
|
||||
).
|
||||
parse_service_args(['--name', Name|Rest], _, Ports, Bootstrap, ServiceType, _) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, ServiceType, create).
|
||||
parse_service_args(['--ports', PortsList|Rest], Name, _, Bootstrap, ServiceType, Action) :-
|
||||
parse_service_args(Rest, Name, PortsList, Bootstrap, ServiceType, Action).
|
||||
parse_service_args(['--bootstrap', BootstrapFile|Rest], Name, Ports, _, ServiceType, Action) :-
|
||||
parse_service_args(Rest, Name, Ports, BootstrapFile, ServiceType, Action).
|
||||
parse_service_args(['--type', Type|Rest], Name, Ports, Bootstrap, _, Action) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, Type, Action).
|
||||
parse_service_args([_|Rest], Name, Ports, Bootstrap, ServiceType, Action) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, ServiceType, Action).
|
||||
parse_service_args(['--name', Name|Rest], _, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles, _, InputFilesOut) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles, create, InputFilesOut).
|
||||
parse_service_args(['--ports', PortsList|Rest], Name, _, Bootstrap, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut) :-
|
||||
parse_service_args(Rest, Name, PortsList, Bootstrap, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut).
|
||||
parse_service_args(['--bootstrap', BootstrapVal|Rest], Name, Ports, _, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut) :-
|
||||
parse_service_args(Rest, Name, Ports, BootstrapVal, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut).
|
||||
parse_service_args(['--bootstrap-file', BootstrapFileVal|Rest], Name, Ports, Bootstrap, _, ServiceType, InputFiles, Action, InputFilesOut) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, BootstrapFileVal, ServiceType, InputFiles, Action, InputFilesOut).
|
||||
parse_service_args(['--type', Type|Rest], Name, Ports, Bootstrap, BootstrapFile, _, InputFiles, Action, InputFilesOut) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, BootstrapFile, Type, InputFiles, Action, InputFilesOut).
|
||||
parse_service_args(['-f', FilePath|Rest], Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut) :-
|
||||
( exists_file(FilePath)
|
||||
-> append(InputFiles, [FilePath], NewInputFiles),
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, BootstrapFile, ServiceType, NewInputFiles, Action, InputFilesOut)
|
||||
; format(user_error, 'Error: File not found: ~w~n', [FilePath]),
|
||||
halt(1)
|
||||
).
|
||||
parse_service_args([_|Rest], Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut) :-
|
||||
parse_service_args(Rest, Name, Ports, Bootstrap, BootstrapFile, ServiceType, InputFiles, Action, InputFilesOut).
|
||||
|
||||
% Execute service action (not used, but kept for structure)
|
||||
execute_service_action(_).
|
||||
execute_service_action(_, _).
|
||||
|
||||
% Main program
|
||||
main(Argv) :-
|
||||
|
|
|
|||
65
un.ps1
65
un.ps1
|
|
@ -193,8 +193,32 @@ function Invoke-Session {
|
|||
$shell = $Args[$idx + 1]
|
||||
}
|
||||
|
||||
$payload = @{ shell = $shell } | ConvertTo-Json
|
||||
$result = Invoke-Api -Endpoint "/sessions" -Method "POST" -Body $payload
|
||||
# Parse input files
|
||||
$inputFiles = @()
|
||||
for ($i = 0; $i -lt $Args.Count; $i++) {
|
||||
if ($Args[$i] -eq "-f" -and ($i + 1) -lt $Args.Count) {
|
||||
$filepath = $Args[$i + 1]
|
||||
if (-not (Test-Path $filepath)) {
|
||||
Write-Error "Error: Input file not found: $filepath"
|
||||
exit 1
|
||||
}
|
||||
$content = [System.IO.File]::ReadAllBytes($filepath)
|
||||
$b64Content = [Convert]::ToBase64String($content)
|
||||
$inputFiles += @{
|
||||
filename = [System.IO.Path]::GetFileName($filepath)
|
||||
content_base64 = $b64Content
|
||||
}
|
||||
$i++
|
||||
}
|
||||
}
|
||||
|
||||
$payload = @{ shell = $shell }
|
||||
if ($inputFiles.Count -gt 0) {
|
||||
$payload["input_files"] = $inputFiles
|
||||
}
|
||||
|
||||
$body = $payload | ConvertTo-Json -Depth 10
|
||||
$result = Invoke-Api -Endpoint "/sessions" -Method "POST" -Body $body
|
||||
Write-Host "`e[33mSession created (WebSocket required for interactive)`e[0m"
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
}
|
||||
|
|
@ -353,12 +377,45 @@ function Invoke-Service {
|
|||
$payload["bootstrap"] = $Args[$bIdx + 1]
|
||||
}
|
||||
|
||||
if ($Args -contains "--bootstrap-file") {
|
||||
$bfIdx = [array]::IndexOf($Args, "--bootstrap-file")
|
||||
$bootstrapFile = $Args[$bfIdx + 1]
|
||||
if (Test-Path $bootstrapFile) {
|
||||
$payload["bootstrap_content"] = Get-Content -Raw $bootstrapFile
|
||||
} else {
|
||||
Write-Error "Error: Bootstrap file not found: $bootstrapFile"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if ($Args -contains "--type") {
|
||||
$tIdx = [array]::IndexOf($Args, "--type")
|
||||
$payload["service_type"] = $Args[$tIdx + 1]
|
||||
}
|
||||
|
||||
$body = $payload | ConvertTo-Json -Depth 5
|
||||
# Parse input files
|
||||
$inputFiles = @()
|
||||
for ($i = 0; $i -lt $Args.Count; $i++) {
|
||||
if ($Args[$i] -eq "-f" -and ($i + 1) -lt $Args.Count) {
|
||||
$filepath = $Args[$i + 1]
|
||||
if (-not (Test-Path $filepath)) {
|
||||
Write-Error "Error: Input file not found: $filepath"
|
||||
exit 1
|
||||
}
|
||||
$content = [System.IO.File]::ReadAllBytes($filepath)
|
||||
$b64Content = [Convert]::ToBase64String($content)
|
||||
$inputFiles += @{
|
||||
filename = [System.IO.Path]::GetFileName($filepath)
|
||||
content_base64 = $b64Content
|
||||
}
|
||||
$i++
|
||||
}
|
||||
}
|
||||
if ($inputFiles.Count -gt 0) {
|
||||
$payload["input_files"] = $inputFiles
|
||||
}
|
||||
|
||||
$body = $payload | ConvertTo-Json -Depth 10
|
||||
$result = Invoke-Api -Endpoint "/services" -Method "POST" -Body $body
|
||||
Write-Host "`e[32mService created`e[0m"
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
|
|
@ -385,12 +442,14 @@ Session options:
|
|||
--list, -l List sessions
|
||||
--kill ID Terminate session
|
||||
--shell NAME Shell/REPL to use
|
||||
-f FILE Input file (can be repeated)
|
||||
|
||||
Service options:
|
||||
--name NAME Service name
|
||||
--ports PORTS Comma-separated ports
|
||||
--type TYPE Service type (minecraft, mumble, teamspeak, source, tcp, udp)
|
||||
--bootstrap CMD Bootstrap command
|
||||
-f FILE Input file (can be repeated)
|
||||
--list, -l List services
|
||||
--info ID Get service info
|
||||
--logs ID Get logs
|
||||
|
|
|
|||
51
un.py
51
un.py
|
|
@ -316,6 +316,23 @@ def cmd_session(args):
|
|||
if args.audit:
|
||||
payload["audit"] = True
|
||||
|
||||
# Add input files
|
||||
if args.files:
|
||||
input_files = []
|
||||
for filepath in args.files:
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
content = base64.b64encode(f.read()).decode('utf-8')
|
||||
input_files.append({
|
||||
"filename": os.path.basename(filepath),
|
||||
"content_base64": content
|
||||
})
|
||||
except FileNotFoundError:
|
||||
print(f"{RED}Error: Input file not found: {filepath}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if input_files:
|
||||
payload["input_files"] = input_files
|
||||
|
||||
print(f"{YELLOW}Creating session...{RESET}")
|
||||
result = api_request("/sessions", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||
print(f"{GREEN}Session created: {result.get('id', 'N/A')}{RESET}")
|
||||
|
|
@ -494,12 +511,28 @@ def cmd_service(args):
|
|||
if args.service_type:
|
||||
payload["service_type"] = args.service_type
|
||||
if args.bootstrap:
|
||||
# Check if bootstrap is a file
|
||||
if os.path.exists(args.bootstrap):
|
||||
with open(args.bootstrap, 'r') as f:
|
||||
payload["bootstrap"] = f.read()
|
||||
else:
|
||||
payload["bootstrap"] = args.bootstrap
|
||||
payload["bootstrap"] = args.bootstrap
|
||||
if args.bootstrap_file:
|
||||
if not os.path.exists(args.bootstrap_file):
|
||||
print(f"{RED}Error: Bootstrap file not found: {args.bootstrap_file}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(args.bootstrap_file, 'r') as f:
|
||||
payload["bootstrap_content"] = f.read()
|
||||
if args.files:
|
||||
input_files = []
|
||||
for filepath in args.files:
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
content = base64.b64encode(f.read()).decode('utf-8')
|
||||
input_files.append({
|
||||
"filename": os.path.basename(filepath),
|
||||
"content_base64": content
|
||||
})
|
||||
except FileNotFoundError:
|
||||
print(f"{RED}Error: Input file not found: {filepath}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if input_files:
|
||||
payload["input_files"] = input_files
|
||||
if args.network:
|
||||
payload["network"] = args.network
|
||||
if args.vcpu:
|
||||
|
|
@ -530,6 +563,7 @@ Examples:
|
|||
%(prog)s session --shell python3 Python REPL
|
||||
%(prog)s session --list List active sessions
|
||||
%(prog)s service --name web --ports 80 --bootstrap "python -m http.server"
|
||||
%(prog)s service --name app --ports 8000 --bootstrap-file ./setup.sh
|
||||
%(prog)s service --list List all services
|
||||
"""
|
||||
)
|
||||
|
|
@ -555,6 +589,7 @@ Examples:
|
|||
session_parser.add_argument("--audit", action="store_true", help="Record session")
|
||||
session_parser.add_argument("--tmux", action="store_true", help="Enable tmux persistence")
|
||||
session_parser.add_argument("--screen", action="store_true", help="Enable screen persistence")
|
||||
session_parser.add_argument("-f", "--files", action="append", metavar="FILE", help="Add input file")
|
||||
session_parser.add_argument("-n", "--network", choices=["zerotrust", "semitrusted"])
|
||||
session_parser.add_argument("-v", "--vcpu", type=int, choices=range(1, 9))
|
||||
session_parser.add_argument("-k", "--api-key")
|
||||
|
|
@ -565,7 +600,9 @@ Examples:
|
|||
service_parser.add_argument("--ports", help="Comma-separated ports")
|
||||
service_parser.add_argument("--domains", help="Comma-separated custom domains")
|
||||
service_parser.add_argument("--type", dest="service_type", help="Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)")
|
||||
service_parser.add_argument("--bootstrap", help="Bootstrap command/file")
|
||||
service_parser.add_argument("--bootstrap", help="Bootstrap command or URI")
|
||||
service_parser.add_argument("--bootstrap-file", dest="bootstrap_file", help="Upload local file as bootstrap script")
|
||||
service_parser.add_argument("-f", "--files", action="append", metavar="FILE", help="Add input file")
|
||||
service_parser.add_argument("-l", "--list", action="store_true", help="List services")
|
||||
service_parser.add_argument("--info", metavar="ID", help="Get service details")
|
||||
service_parser.add_argument("--tail", metavar="ID", help="Get last 9000 lines of logs")
|
||||
|
|
|
|||
67
un.r
67
un.r
|
|
@ -281,8 +281,36 @@ cmd_session <- function(args) {
|
|||
return()
|
||||
}
|
||||
|
||||
cat(sprintf("%sError: Use --list or --kill%s\n", RED, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
# Create new session
|
||||
payload <- list(shell = "bash")
|
||||
|
||||
if (!is.null(args$network)) {
|
||||
payload$network <- args$network
|
||||
}
|
||||
|
||||
# Add input files
|
||||
if (!is.null(args$files)) {
|
||||
input_files <- list()
|
||||
for (filepath in args$files) {
|
||||
if (!file.exists(filepath)) {
|
||||
cat(sprintf("%sError: Input file not found: %s%s\n", RED, filepath, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
}
|
||||
content <- base64enc::base64encode(filepath)
|
||||
input_files[[length(input_files) + 1]] <- list(
|
||||
filename = basename(filepath),
|
||||
content_base64 = content
|
||||
)
|
||||
}
|
||||
if (length(input_files) > 0) {
|
||||
payload$input_files <- input_files
|
||||
}
|
||||
}
|
||||
|
||||
cat(sprintf("%sCreating session...%s\n", YELLOW, RESET))
|
||||
result <- api_request("/sessions", public_key, secret_key, method = "POST", data = payload)
|
||||
cat(sprintf("%sSession created: %s%s\n", GREEN, if (!is.null(result$id)) result$id else "N/A", RESET))
|
||||
cat(sprintf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n", YELLOW, RESET))
|
||||
}
|
||||
|
||||
cmd_key <- function(args) {
|
||||
|
|
@ -485,10 +513,15 @@ cmd_service <- function(args) {
|
|||
}
|
||||
|
||||
if (!is.null(args$bootstrap)) {
|
||||
if (file.exists(args$bootstrap)) {
|
||||
payload$bootstrap <- paste(readLines(args$bootstrap, warn = FALSE), collapse = "\n")
|
||||
payload$bootstrap <- args$bootstrap
|
||||
}
|
||||
|
||||
if (!is.null(args$bootstrap_file)) {
|
||||
if (file.exists(args$bootstrap_file)) {
|
||||
payload$bootstrap_content <- paste(readLines(args$bootstrap_file, warn = FALSE), collapse = "\n")
|
||||
} else {
|
||||
payload$bootstrap <- args$bootstrap
|
||||
cat(sprintf("%sError: Bootstrap file not found: %s%s\n", RED, args$bootstrap_file, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -500,6 +533,25 @@ cmd_service <- function(args) {
|
|||
payload$vcpu <- args$vcpu
|
||||
}
|
||||
|
||||
# Add input files
|
||||
if (!is.null(args$files)) {
|
||||
input_files <- list()
|
||||
for (filepath in args$files) {
|
||||
if (!file.exists(filepath)) {
|
||||
cat(sprintf("%sError: Input file not found: %s%s\n", RED, filepath, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
}
|
||||
content <- base64enc::base64encode(filepath)
|
||||
input_files[[length(input_files) + 1]] <- list(
|
||||
filename = basename(filepath),
|
||||
content_base64 = content
|
||||
)
|
||||
}
|
||||
if (length(input_files) > 0) {
|
||||
payload$input_files <- input_files
|
||||
}
|
||||
}
|
||||
|
||||
result <- api_request("/services", public_key, secret_key, method = "POST", data = payload)
|
||||
cat(sprintf("%sService created: %s%s\n", GREEN, if (!is.null(result$id)) result$id else "N/A", RESET))
|
||||
cat(sprintf("Name: %s\n", if (!is.null(result$name)) result$name else "N/A"))
|
||||
|
|
@ -539,6 +591,7 @@ parse_args <- function() {
|
|||
domains = NULL,
|
||||
type = NULL,
|
||||
bootstrap = NULL,
|
||||
bootstrap_file = NULL,
|
||||
vcpu = NULL,
|
||||
extend = FALSE
|
||||
)
|
||||
|
|
@ -634,6 +687,10 @@ parse_args <- function() {
|
|||
i <- i + 1
|
||||
result$bootstrap <- args[i]
|
||||
i <- i + 1
|
||||
} else if (arg == "--bootstrap-file") {
|
||||
i <- i + 1
|
||||
result$bootstrap_file <- args[i]
|
||||
i <- i + 1
|
||||
} else if (arg %in% c("-v", "--vcpu")) {
|
||||
i <- i + 1
|
||||
result$vcpu <- as.integer(args[i])
|
||||
|
|
|
|||
61
un.raku
61
un.raku
|
|
@ -283,6 +283,7 @@ sub cmd-session(@args) {
|
|||
my $shell = '';
|
||||
my $network = '';
|
||||
my $vcpu = 0;
|
||||
my @input-files;
|
||||
|
||||
# Parse arguments
|
||||
my $i = 0;
|
||||
|
|
@ -307,6 +308,10 @@ sub cmd-session(@args) {
|
|||
$i++;
|
||||
$vcpu = @args[$i].Int;
|
||||
}
|
||||
when '-f' {
|
||||
$i++;
|
||||
@input-files.push(@args[$i]);
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
|
@ -337,6 +342,23 @@ sub cmd-session(@args) {
|
|||
%payload<network> = $network if $network;
|
||||
%payload<vcpu> = $vcpu if $vcpu > 0;
|
||||
|
||||
# Add input files
|
||||
if @input-files {
|
||||
my @files;
|
||||
for @input-files -> $filepath {
|
||||
unless $filepath.IO.e {
|
||||
note "{$RED}Error: Input file not found: $filepath{$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
my $content = $filepath.IO.slurp(:bin);
|
||||
@files.push({
|
||||
filename => $filepath.IO.basename,
|
||||
content_base64 => $content.encode('latin1').decode('latin1').encode.base64
|
||||
});
|
||||
}
|
||||
%payload<input_files> = @files;
|
||||
}
|
||||
|
||||
say "{$YELLOW}Creating session...{$RESET}";
|
||||
my %result = api-request('/sessions', 'POST', %payload, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Session created: {%result<id>}{$RESET}";
|
||||
|
|
@ -357,8 +379,10 @@ sub cmd-service(@args) {
|
|||
my $ports = '';
|
||||
my $type = '';
|
||||
my $bootstrap = '';
|
||||
my $bootstrap-file = '';
|
||||
my $network = '';
|
||||
my $vcpu = 0;
|
||||
my @input-files;
|
||||
|
||||
# Parse arguments
|
||||
my $i = 0;
|
||||
|
|
@ -411,6 +435,10 @@ sub cmd-service(@args) {
|
|||
$i++;
|
||||
$bootstrap = @args[$i];
|
||||
}
|
||||
when '--bootstrap-file' {
|
||||
$i++;
|
||||
$bootstrap-file = @args[$i];
|
||||
}
|
||||
when '-n' {
|
||||
$i++;
|
||||
$network = @args[$i];
|
||||
|
|
@ -419,6 +447,10 @@ sub cmd-service(@args) {
|
|||
$i++;
|
||||
$vcpu = @args[$i].Int;
|
||||
}
|
||||
when '-f' {
|
||||
$i++;
|
||||
@input-files.push(@args[$i]);
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
|
@ -506,17 +538,38 @@ sub cmd-service(@args) {
|
|||
}
|
||||
|
||||
if $bootstrap {
|
||||
# Check if bootstrap is a file
|
||||
if $bootstrap.IO.e && $bootstrap.IO.f {
|
||||
%payload<bootstrap> = $bootstrap.IO.slurp;
|
||||
%payload<bootstrap> = $bootstrap;
|
||||
}
|
||||
|
||||
if $bootstrap-file {
|
||||
if $bootstrap-file.IO.e && $bootstrap-file.IO.f {
|
||||
%payload<bootstrap_content> = $bootstrap-file.IO.slurp;
|
||||
} else {
|
||||
%payload<bootstrap> = $bootstrap;
|
||||
note "{$RED}Error: Bootstrap file not found: $bootstrap-file{$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
}
|
||||
|
||||
%payload<network> = $network if $network;
|
||||
%payload<vcpu> = $vcpu if $vcpu > 0;
|
||||
|
||||
# Add input files
|
||||
if @input-files {
|
||||
my @files;
|
||||
for @input-files -> $filepath {
|
||||
unless $filepath.IO.e {
|
||||
note "{$RED}Error: Input file not found: $filepath{$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
my $content = $filepath.IO.slurp(:bin);
|
||||
@files.push({
|
||||
filename => $filepath.IO.basename,
|
||||
content_base64 => $content.encode('latin1').decode('latin1').encode.base64
|
||||
});
|
||||
}
|
||||
%payload<input_files> = @files;
|
||||
}
|
||||
|
||||
my %result = api-request('/services', 'POST', %payload, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Service created: {%result<id>}{$RESET}";
|
||||
say "Name: {%result<name>}";
|
||||
|
|
|
|||
48
un.rb
48
un.rb
|
|
@ -268,6 +268,21 @@ def cmd_session(options)
|
|||
payload[:persistence] = 'screen' if options[:screen]
|
||||
payload[:audit] = true if options[:audit]
|
||||
|
||||
# Add input files
|
||||
if options[:files] && !options[:files].empty?
|
||||
input_files = options[:files].map do |filepath|
|
||||
unless File.exist?(filepath)
|
||||
warn "#{RED}Error: Input file not found: #{filepath}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
{
|
||||
filename: File.basename(filepath),
|
||||
content_base64: Base64.strict_encode64(File.read(filepath, mode: 'rb'))
|
||||
}
|
||||
end
|
||||
payload[:input_files] = input_files
|
||||
end
|
||||
|
||||
puts "#{YELLOW}Creating session...#{RESET}"
|
||||
result = api_request('/sessions', method: 'POST', data: payload, keys: keys)
|
||||
puts "#{GREEN}Session created: #{result['id'] || 'N/A'}#{RESET}"
|
||||
|
|
@ -461,12 +476,27 @@ def cmd_service(options)
|
|||
payload[:ports] = options[:ports].split(',').map(&:to_i) if options[:ports]
|
||||
payload[:domains] = options[:domains].split(',') if options[:domains]
|
||||
payload[:service_type] = options[:type] if options[:type]
|
||||
if options[:bootstrap]
|
||||
payload[:bootstrap] = if File.exist?(options[:bootstrap])
|
||||
File.read(options[:bootstrap])
|
||||
else
|
||||
options[:bootstrap]
|
||||
end
|
||||
payload[:bootstrap] = options[:bootstrap] if options[:bootstrap]
|
||||
if options[:bootstrap_file]
|
||||
unless File.exist?(options[:bootstrap_file])
|
||||
warn "#{RED}Error: Bootstrap file not found: #{options[:bootstrap_file]}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
payload[:bootstrap_content] = File.read(options[:bootstrap_file])
|
||||
end
|
||||
# Add input files
|
||||
if options[:files] && !options[:files].empty?
|
||||
input_files = options[:files].map do |filepath|
|
||||
unless File.exist?(filepath)
|
||||
warn "#{RED}Error: Input file not found: #{filepath}#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
{
|
||||
filename: File.basename(filepath),
|
||||
content_base64: Base64.strict_encode64(File.read(filepath, mode: 'rb'))
|
||||
}
|
||||
end
|
||||
payload[:input_files] = input_files
|
||||
end
|
||||
payload[:network] = options[:network] if options[:network]
|
||||
payload[:vcpu] = options[:vcpu] if options[:vcpu]
|
||||
|
|
@ -577,6 +607,9 @@ def main
|
|||
when '--bootstrap'
|
||||
i += 1
|
||||
options[:bootstrap] = ARGV[i]
|
||||
when '--bootstrap-file'
|
||||
i += 1
|
||||
options[:bootstrap_file] = ARGV[i]
|
||||
when '--info'
|
||||
i += 1
|
||||
options[:info] = ARGV[i]
|
||||
|
|
@ -659,7 +692,8 @@ def main
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
83
un.rs
83
un.rs
|
|
@ -348,6 +348,7 @@ fn cmd_session(
|
|||
vcpu: Option<i32>,
|
||||
tmux: bool,
|
||||
screen: bool,
|
||||
files: &[String],
|
||||
public_key: &str,
|
||||
secret_key: &str,
|
||||
) {
|
||||
|
|
@ -381,6 +382,25 @@ fn cmd_session(
|
|||
if screen {
|
||||
json.push_str(r#","persistence":"screen""#);
|
||||
}
|
||||
|
||||
// Input files
|
||||
if !files.is_empty() {
|
||||
json.push_str(r#","input_files":["#);
|
||||
for (i, f) in files.iter().enumerate() {
|
||||
if i > 0 {
|
||||
json.push(',');
|
||||
}
|
||||
let content = fs::read(f).unwrap_or_else(|e| {
|
||||
eprintln!("{}Error reading input file {}: {}{}", RED, f, e, RESET);
|
||||
process::exit(1);
|
||||
});
|
||||
let b64 = base64::encode(&content);
|
||||
let filename = Path::new(f).file_name().map(|n| n.to_string_lossy()).unwrap_or_default();
|
||||
json.push_str(&format!(r#"{{"filename":"{}","content_base64":"{}"}}"#, filename, b64));
|
||||
}
|
||||
json.push(']');
|
||||
}
|
||||
|
||||
json.push('}');
|
||||
|
||||
println!("{}Creating session...{}", YELLOW, RESET);
|
||||
|
|
@ -395,6 +415,8 @@ fn cmd_service(
|
|||
domains: Option<&str>,
|
||||
service_type: Option<&str>,
|
||||
bootstrap: Option<&str>,
|
||||
bootstrap_file: Option<&str>,
|
||||
files: &[String],
|
||||
list: bool,
|
||||
info: Option<&str>,
|
||||
logs: Option<&str>,
|
||||
|
|
@ -533,12 +555,38 @@ fn cmd_service(
|
|||
}
|
||||
|
||||
if let Some(b) = bootstrap {
|
||||
let cmd = if Path::new(b).exists() {
|
||||
fs::read_to_string(b).unwrap_or(b.to_string())
|
||||
json.push_str(&format!(r#","bootstrap":"{}""#, escape_json(b)));
|
||||
}
|
||||
|
||||
if let Some(bf) = bootstrap_file {
|
||||
if Path::new(bf).exists() {
|
||||
let content = fs::read_to_string(bf).unwrap_or_else(|e| {
|
||||
eprintln!("{}Error reading bootstrap file: {}{}", RED, e, RESET);
|
||||
process::exit(1);
|
||||
});
|
||||
json.push_str(&format!(r#","bootstrap_content":"{}""#, escape_json(&content)));
|
||||
} else {
|
||||
b.to_string()
|
||||
};
|
||||
json.push_str(&format!(r#","bootstrap":"{}""#, escape_json(&cmd)));
|
||||
eprintln!("{}Error: Bootstrap file not found: {}{}", RED, bf, RESET);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Input files
|
||||
if !files.is_empty() {
|
||||
json.push_str(r#","input_files":["#);
|
||||
for (i, f) in files.iter().enumerate() {
|
||||
if i > 0 {
|
||||
json.push(',');
|
||||
}
|
||||
let content = fs::read(f).unwrap_or_else(|e| {
|
||||
eprintln!("{}Error reading input file {}: {}{}", RED, f, e, RESET);
|
||||
process::exit(1);
|
||||
});
|
||||
let b64 = base64::encode(&content);
|
||||
let filename = Path::new(f).file_name().map(|n| n.to_string_lossy()).unwrap_or_default();
|
||||
json.push_str(&format!(r#"{{"filename":"{}","content_base64":"{}"}}"#, filename, b64));
|
||||
}
|
||||
json.push(']');
|
||||
}
|
||||
|
||||
if let Some(net) = network {
|
||||
|
|
@ -703,6 +751,17 @@ fn main() {
|
|||
}
|
||||
"session" => {
|
||||
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
|
||||
// Collect -f files for session
|
||||
let mut session_files: Vec<String> = Vec::new();
|
||||
let mut j = i + 1;
|
||||
while j < args.len() {
|
||||
if args[j] == "-f" && j + 1 < args.len() {
|
||||
session_files.push(args[j + 1].clone());
|
||||
j += 2;
|
||||
} else {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
cmd_session(
|
||||
args.contains(&"--list".to_string()),
|
||||
args.iter().position(|x| x == "--kill").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
|
|
@ -711,6 +770,7 @@ fn main() {
|
|||
vcpu,
|
||||
args.contains(&"--tmux".to_string()),
|
||||
args.contains(&"--screen".to_string()),
|
||||
&session_files,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
);
|
||||
|
|
@ -718,12 +778,25 @@ fn main() {
|
|||
}
|
||||
"service" => {
|
||||
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
|
||||
// Collect -f files for service
|
||||
let mut service_files: Vec<String> = Vec::new();
|
||||
let mut j = i + 1;
|
||||
while j < args.len() {
|
||||
if args[j] == "-f" && j + 1 < args.len() {
|
||||
service_files.push(args[j + 1].clone());
|
||||
j += 2;
|
||||
} else {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
cmd_service(
|
||||
args.iter().position(|x| x == "--name").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
args.iter().position(|x| x == "--ports").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
args.iter().position(|x| x == "--domains").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
args.iter().position(|x| x == "--type").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
args.iter().position(|x| x == "--bootstrap").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
args.iter().position(|x| x == "--bootstrap-file").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
&service_files,
|
||||
args.contains(&"--list".to_string()),
|
||||
args.iter().position(|x| x == "--info").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
args.iter().position(|x| x == "--logs").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
|
|
|
|||
121
un.scm
121
un.scm
|
|
@ -93,6 +93,30 @@
|
|||
(string-join (reverse lines) "\n")
|
||||
(loop (cons line lines))))))))
|
||||
|
||||
(define (base64-encode-file filename)
|
||||
"Base64 encode a file using shell command"
|
||||
(let* ((cmd (format #f "base64 -w0 ~a" filename))
|
||||
(port (open-input-pipe cmd))
|
||||
(result (let loop ((chars '()))
|
||||
(let ((char (read-char port)))
|
||||
(if (eof-object? char)
|
||||
(list->string (reverse chars))
|
||||
(loop (cons char chars)))))))
|
||||
(close-pipe port)
|
||||
(string-trim-both result)))
|
||||
|
||||
(define (build-input-files-json files)
|
||||
"Build input_files JSON array from list of filenames"
|
||||
(if (null? files)
|
||||
""
|
||||
(let ((entries (map (lambda (f)
|
||||
(let* ((basename (basename f))
|
||||
(content (base64-encode-file f)))
|
||||
(format #f "{\"filename\":\"~a\",\"content\":\"~a\"}"
|
||||
basename content)))
|
||||
files)))
|
||||
(format #f ",\"input_files\":[~a]" (string-join entries ",")))))
|
||||
|
||||
(define (write-temp-file data)
|
||||
(let ((tmp-file (format #f "/tmp/un_scm_~a.json" (random 999999))))
|
||||
(call-with-output-file tmp-file
|
||||
|
|
@ -340,7 +364,7 @@
|
|||
(display response)
|
||||
(newline))))
|
||||
|
||||
(define (session-cmd action id shell)
|
||||
(define (session-cmd action id shell input-files)
|
||||
(let ((api-key (get-api-key)))
|
||||
(cond
|
||||
((equal? action "list")
|
||||
|
|
@ -351,13 +375,14 @@
|
|||
(format #t "~aSession terminated: ~a~a\n" green id reset))
|
||||
(else
|
||||
(let* ((sh (or shell "bash"))
|
||||
(json (format #f "{\"shell\":\"~a\"}" sh))
|
||||
(input-files-json (build-input-files-json input-files))
|
||||
(json (format #f "{\"shell\":\"~a\"~a}" sh input-files-json))
|
||||
(response (curl-post api-key "/sessions" json)))
|
||||
(format #t "~aSession created (WebSocket required)~a\n" yellow reset)
|
||||
(display response)
|
||||
(newline))))))
|
||||
|
||||
(define (service-cmd action id name ports bootstrap type)
|
||||
(define (service-cmd action id name ports bootstrap bootstrap-file type input-files)
|
||||
(let ((api-key (get-api-key)))
|
||||
(cond
|
||||
((equal? action "list")
|
||||
|
|
@ -405,8 +430,12 @@
|
|||
((and (equal? action "create") name)
|
||||
(let* ((ports-json (if ports (format #f ",\"ports\":[~a]" ports) ""))
|
||||
(bootstrap-json (if bootstrap (format #f ",\"bootstrap\":\"~a\"" (escape-json bootstrap)) ""))
|
||||
(bootstrap-content-json (if bootstrap-file
|
||||
(format #f ",\"bootstrap_content\":\"~a\"" (escape-json (read-file bootstrap-file)))
|
||||
""))
|
||||
(type-json (if type (format #f ",\"service_type\":\"~a\"" type) ""))
|
||||
(json (format #f "{\"name\":\"~a\"~a~a~a}" name ports-json bootstrap-json type-json))
|
||||
(input-files-json (build-input-files-json input-files))
|
||||
(json (format #f "{\"name\":\"~a\"~a~a~a~a~a}" name ports-json bootstrap-json bootstrap-content-json type-json input-files-json))
|
||||
(response (curl-post api-key "/services" json)))
|
||||
(format #t "~aService created~a\n" green reset)
|
||||
(display response)
|
||||
|
|
@ -415,6 +444,20 @@
|
|||
(display "Error: --name required to create service\n" (current-error-port))
|
||||
(exit 1)))))
|
||||
|
||||
(define (parse-input-files args)
|
||||
"Parse -f flags from args and return list of filenames"
|
||||
(let loop ((args args) (files '()))
|
||||
(if (null? args)
|
||||
(reverse files)
|
||||
(if (and (equal? (car args) "-f") (pair? (cdr args)))
|
||||
(let ((file (cadr args)))
|
||||
(if (file-exists? file)
|
||||
(loop (cddr args) (cons file files))
|
||||
(begin
|
||||
(format (current-error-port) "Error: File not found: ~a\n" file)
|
||||
(exit 1))))
|
||||
(loop (cdr args) files)))))
|
||||
|
||||
(define (main args)
|
||||
(if (null? args)
|
||||
(begin
|
||||
|
|
@ -429,39 +472,69 @@
|
|||
(validate-key-cmd extend)))
|
||||
((equal? (car args) "session")
|
||||
(if (and (> (length args) 1) (equal? (cadr args) "--list"))
|
||||
(session-cmd "list" #f #f)
|
||||
(session-cmd "list" #f #f '())
|
||||
(if (and (> (length args) 2) (equal? (cadr args) "--kill"))
|
||||
(session-cmd "kill" (caddr args) #f)
|
||||
(session-cmd "create" #f #f))))
|
||||
(session-cmd "kill" (caddr args) #f '())
|
||||
;; Parse session create options including -f
|
||||
(let* ((rest-args (cdr args))
|
||||
(input-files (parse-input-files rest-args))
|
||||
(shell #f))
|
||||
;; Parse --shell option
|
||||
(let loop ((args rest-args))
|
||||
(when (and (pair? args) (pair? (cdr args)))
|
||||
(cond
|
||||
((or (equal? (car args) "--shell") (equal? (car args) "-s"))
|
||||
(set! shell (cadr args)))
|
||||
(else (loop (cdr args))))))
|
||||
(session-cmd "create" #f shell input-files)))))
|
||||
((equal? (car args) "service")
|
||||
(cond
|
||||
((and (> (length args) 1) (equal? (cadr args) "--list"))
|
||||
(service-cmd "list" #f #f #f #f #f))
|
||||
(service-cmd "list" #f #f #f #f #f #f '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--info"))
|
||||
(service-cmd "info" (caddr args) #f #f #f #f))
|
||||
(service-cmd "info" (caddr args) #f #f #f #f #f '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--logs"))
|
||||
(service-cmd "logs" (caddr args) #f #f #f #f))
|
||||
(service-cmd "logs" (caddr args) #f #f #f #f #f '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--freeze"))
|
||||
(service-cmd "sleep" (caddr args) #f #f #f #f))
|
||||
(service-cmd "sleep" (caddr args) #f #f #f #f #f '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--unfreeze"))
|
||||
(service-cmd "wake" (caddr args) #f #f #f #f))
|
||||
(service-cmd "wake" (caddr args) #f #f #f #f #f '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--destroy"))
|
||||
(service-cmd "destroy" (caddr args) #f #f #f #f))
|
||||
(service-cmd "destroy" (caddr args) #f #f #f #f #f '()))
|
||||
((and (> (length args) 3) (equal? (cadr args) "--execute"))
|
||||
(service-cmd "execute" (caddr args) #f #f (list-ref args 3) #f))
|
||||
(service-cmd "execute" (caddr args) #f #f (list-ref args 3) #f #f '()))
|
||||
((and (> (length args) 3) (equal? (cadr args) "--dump-bootstrap"))
|
||||
(service-cmd "dump-bootstrap" (caddr args) #f #f #f (list-ref args 3)))
|
||||
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f (list-ref args 3) '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--dump-bootstrap"))
|
||||
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f))
|
||||
(service-cmd "dump-bootstrap" (caddr args) #f #f #f #f #f '()))
|
||||
((and (> (length args) 2) (equal? (cadr args) "--name"))
|
||||
(let ((name (caddr args))
|
||||
(ports (if (and (> (length args) 4) (equal? (list-ref args 3) "--ports"))
|
||||
(list-ref args 4) #f))
|
||||
(bootstrap (if (and (> (length args) 6) (equal? (list-ref args 5) "--bootstrap"))
|
||||
(list-ref args 6) #f))
|
||||
(type (if (and (> (length args) 8) (equal? (list-ref args 7) "--type"))
|
||||
(list-ref args 8) #f)))
|
||||
(service-cmd "create" #f name ports bootstrap type)))
|
||||
(let* ((name (caddr args))
|
||||
(rest-args (cdddr args))
|
||||
(ports #f)
|
||||
(bootstrap #f)
|
||||
(bootstrap-file #f)
|
||||
(type #f)
|
||||
(input-files (parse-input-files rest-args)))
|
||||
;; Parse remaining args
|
||||
(let loop ((args rest-args))
|
||||
(when (and (pair? args) (pair? (cdr args)))
|
||||
(cond
|
||||
((equal? (car args) "--ports")
|
||||
(set! ports (cadr args))
|
||||
(loop (cddr args)))
|
||||
((equal? (car args) "--bootstrap")
|
||||
(set! bootstrap (cadr args))
|
||||
(loop (cddr args)))
|
||||
((equal? (car args) "--bootstrap-file")
|
||||
(set! bootstrap-file (cadr args))
|
||||
(loop (cddr args)))
|
||||
((equal? (car args) "--type")
|
||||
(set! type (cadr args))
|
||||
(loop (cddr args)))
|
||||
((equal? (car args) "-f")
|
||||
(loop (cddr args))) ; skip -f, already parsed
|
||||
(else (loop (cdr args))))))
|
||||
(service-cmd "create" #f name ports bootstrap bootstrap-file type input-files)))
|
||||
(else
|
||||
(display "Error: Invalid service command\n" (current-error-port))
|
||||
(exit 1))))
|
||||
|
|
|
|||
64
un.sh
64
un.sh
|
|
@ -343,6 +343,7 @@ cmd_session() {
|
|||
local network=""
|
||||
local vcpu=""
|
||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||
local -a input_files=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -374,6 +375,10 @@ cmd_session() {
|
|||
screen=true
|
||||
shift
|
||||
;;
|
||||
-f)
|
||||
input_files+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
-n)
|
||||
network="$2"
|
||||
shift 2
|
||||
|
|
@ -427,6 +432,22 @@ cmd_session() {
|
|||
[[ "$screen" == true ]] && payload=$(echo "$payload" | jq '. + {persistence: "screen"}')
|
||||
[[ "$audit" == true ]] && payload=$(echo "$payload" | jq '. + {audit: true}')
|
||||
|
||||
# Add input files
|
||||
if [[ ${#input_files[@]} -gt 0 ]]; then
|
||||
local files_json="["
|
||||
for file in "${input_files[@]}"; do
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo -e "${RED}Error: Input file not found: $file${RESET}" >&2
|
||||
exit 1
|
||||
fi
|
||||
local filename=$(basename "$file")
|
||||
local content_b64=$(base64 -w0 < "$file")
|
||||
files_json+="{\"filename\":\"$filename\",\"content_base64\":\"$content_b64\"},"
|
||||
done
|
||||
files_json="${files_json%,}]"
|
||||
payload=$(echo "$payload" | jq --argjson files "$files_json" '. + {input_files: $files}')
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Creating session...${RESET}"
|
||||
local result=$(api_request "/sessions" "POST" "$payload" "$api_key")
|
||||
local session_id=$(echo "$result" | jq -r '.id // "N/A"')
|
||||
|
|
@ -440,6 +461,7 @@ cmd_service() {
|
|||
local domains=""
|
||||
local service_type=""
|
||||
local bootstrap=""
|
||||
local bootstrap_file=""
|
||||
local list=false
|
||||
local info=""
|
||||
local logs=""
|
||||
|
|
@ -452,6 +474,7 @@ cmd_service() {
|
|||
local network=""
|
||||
local vcpu=""
|
||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||
local -a input_files=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -475,6 +498,14 @@ cmd_service() {
|
|||
bootstrap="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bootstrap-file)
|
||||
bootstrap_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-f)
|
||||
input_files+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
-l|--list)
|
||||
list=true
|
||||
shift
|
||||
|
|
@ -639,12 +670,32 @@ cmd_service() {
|
|||
fi
|
||||
|
||||
if [[ -n "$bootstrap" ]]; then
|
||||
if [[ -f "$bootstrap" ]]; then
|
||||
local bootstrap_content=$(cat "$bootstrap")
|
||||
payload=$(echo "$payload" | jq --arg b "$bootstrap_content" '. + {bootstrap: $b}')
|
||||
else
|
||||
payload=$(echo "$payload" | jq --arg b "$bootstrap" '. + {bootstrap: $b}')
|
||||
payload=$(echo "$payload" | jq --arg b "$bootstrap" '. + {bootstrap: $b}')
|
||||
fi
|
||||
|
||||
if [[ -n "$bootstrap_file" ]]; then
|
||||
if [[ ! -f "$bootstrap_file" ]]; then
|
||||
echo -e "${RED}Error: Bootstrap file not found: $bootstrap_file${RESET}" >&2
|
||||
return 1
|
||||
fi
|
||||
local file_content=$(cat "$bootstrap_file")
|
||||
payload=$(echo "$payload" | jq --arg b "$file_content" '. + {bootstrap_content: $b}')
|
||||
fi
|
||||
|
||||
# Add input files
|
||||
if [[ ${#input_files[@]} -gt 0 ]]; then
|
||||
local files_json="["
|
||||
for file in "${input_files[@]}"; do
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo -e "${RED}Error: Input file not found: $file${RESET}" >&2
|
||||
exit 1
|
||||
fi
|
||||
local filename=$(basename "$file")
|
||||
local content_b64=$(base64 -w0 < "$file")
|
||||
files_json+="{\"filename\":\"$filename\",\"content_base64\":\"$content_b64\"},"
|
||||
done
|
||||
files_json="${files_json%,}]"
|
||||
payload=$(echo "$payload" | jq --argjson files "$files_json" '. + {input_files: $files}')
|
||||
fi
|
||||
|
||||
[[ -n "$network" ]] && payload=$(echo "$payload" | jq --arg n "$network" '. + {network: $n}')
|
||||
|
|
@ -842,7 +893,8 @@ Service options:
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type for SRV records (minecraft, mumble, teamspeak, source, tcp, udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
67
un.tcl
67
un.tcl
|
|
@ -319,6 +319,7 @@ proc cmd_session {args} {
|
|||
set shell ""
|
||||
set network ""
|
||||
set vcpu 0
|
||||
set input_files [list]
|
||||
|
||||
# Parse arguments
|
||||
for {set i 0} {$i < [llength $args]} {incr i} {
|
||||
|
|
@ -343,6 +344,10 @@ proc cmd_session {args} {
|
|||
incr i
|
||||
set vcpu [lindex $args $i]
|
||||
}
|
||||
-f {
|
||||
incr i
|
||||
lappend input_files [lindex $args $i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -384,6 +389,25 @@ proc cmd_session {args} {
|
|||
lappend payload vcpu $vcpu
|
||||
}
|
||||
|
||||
# Add input files
|
||||
if {[llength $input_files] > 0} {
|
||||
set files_json [list]
|
||||
foreach filepath $input_files {
|
||||
if {![file exists $filepath]} {
|
||||
puts stderr "${::RED}Error: Input file not found: $filepath${::RESET}"
|
||||
exit 1
|
||||
}
|
||||
set fp [open $filepath rb]
|
||||
set content [read $fp]
|
||||
close $fp
|
||||
set b64_content [::base64::encode $content]
|
||||
lappend files_json [::json::write object \
|
||||
filename [::json::write string [file tail $filepath]] \
|
||||
content_base64 [::json::write string $b64_content]]
|
||||
}
|
||||
lappend payload input_files [::json::write array {*}$files_json]
|
||||
}
|
||||
|
||||
puts "${::YELLOW}Creating session...${::RESET}"
|
||||
set result [api_request "/sessions" "POST" $payload $public_key $secret_key]
|
||||
puts "${::GREEN}Session created: [dict get $result id]${::RESET}"
|
||||
|
|
@ -498,8 +522,10 @@ proc cmd_service {args} {
|
|||
set ports ""
|
||||
set service_type ""
|
||||
set bootstrap ""
|
||||
set bootstrap_file ""
|
||||
set network ""
|
||||
set vcpu 0
|
||||
set input_files [list]
|
||||
|
||||
# Parse arguments
|
||||
for {set i 0} {$i < [llength $args]} {incr i} {
|
||||
|
|
@ -552,6 +578,10 @@ proc cmd_service {args} {
|
|||
incr i
|
||||
set bootstrap [lindex $args $i]
|
||||
}
|
||||
--bootstrap-file {
|
||||
incr i
|
||||
set bootstrap_file [lindex $args $i]
|
||||
}
|
||||
-n {
|
||||
incr i
|
||||
set network [lindex $args $i]
|
||||
|
|
@ -560,6 +590,10 @@ proc cmd_service {args} {
|
|||
incr i
|
||||
set vcpu [lindex $args $i]
|
||||
}
|
||||
-f {
|
||||
incr i
|
||||
lappend input_files [lindex $args $i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -657,14 +691,18 @@ proc cmd_service {args} {
|
|||
}
|
||||
|
||||
if {$bootstrap ne ""} {
|
||||
# Check if bootstrap is a file
|
||||
if {[file exists $bootstrap]} {
|
||||
set fp [open $bootstrap r]
|
||||
lappend payload bootstrap [::json::write string $bootstrap]
|
||||
}
|
||||
|
||||
if {$bootstrap_file ne ""} {
|
||||
if {[file exists $bootstrap_file]} {
|
||||
set fp [open $bootstrap_file r]
|
||||
set bootstrap_content [read $fp]
|
||||
close $fp
|
||||
lappend payload bootstrap [::json::write string $bootstrap_content]
|
||||
lappend payload bootstrap_content [::json::write string $bootstrap_content]
|
||||
} else {
|
||||
lappend payload bootstrap [::json::write string $bootstrap]
|
||||
puts stderr "${::RED}Error: Bootstrap file not found: $bootstrap_file${::RESET}"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -675,6 +713,25 @@ proc cmd_service {args} {
|
|||
lappend payload vcpu $vcpu
|
||||
}
|
||||
|
||||
# Add input files
|
||||
if {[llength $input_files] > 0} {
|
||||
set files_json [list]
|
||||
foreach filepath $input_files {
|
||||
if {![file exists $filepath]} {
|
||||
puts stderr "${::RED}Error: Input file not found: $filepath${::RESET}"
|
||||
exit 1
|
||||
}
|
||||
set fp [open $filepath rb]
|
||||
set content [read $fp]
|
||||
close $fp
|
||||
set b64_content [::base64::encode $content]
|
||||
lappend files_json [::json::write object \
|
||||
filename [::json::write string [file tail $filepath]] \
|
||||
content_base64 [::json::write string $b64_content]]
|
||||
}
|
||||
lappend payload input_files [::json::write array {*}$files_json]
|
||||
}
|
||||
|
||||
set result [api_request "/services" "POST" $payload $public_key $secret_key]
|
||||
puts "${::GREEN}Service created: [dict get $result id]${::RESET}"
|
||||
puts "Name: [dict get $result name]"
|
||||
|
|
|
|||
50
un.ts
50
un.ts
|
|
@ -102,6 +102,7 @@ interface Args {
|
|||
domains: string | null;
|
||||
type: string | null;
|
||||
bootstrap: string | null;
|
||||
bootstrapFile: string | null;
|
||||
info: string | null;
|
||||
logs: string | null;
|
||||
tail: string | null;
|
||||
|
|
@ -363,6 +364,22 @@ async function cmdSession(args: Args): Promise<void> {
|
|||
if (args.screen) payload.persistence = "screen";
|
||||
if (args.audit) payload.audit = true;
|
||||
|
||||
// Add input files
|
||||
if (args.files && args.files.length > 0) {
|
||||
payload.input_files = args.files.map(filepath => {
|
||||
try {
|
||||
const content = fs.readFileSync(filepath);
|
||||
return {
|
||||
filename: path.basename(filepath),
|
||||
content_base64: content.toString('base64')
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`${YELLOW}Creating session...${RESET}`);
|
||||
const result = await apiRequest("/sessions", "POST", payload, keys);
|
||||
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
|
||||
|
|
@ -466,11 +483,29 @@ async function cmdService(args: Args): Promise<void> {
|
|||
if (args.domains) payload.domains = args.domains.split(',');
|
||||
if (args.type) payload.service_type = args.type;
|
||||
if (args.bootstrap) {
|
||||
if (fs.existsSync(args.bootstrap)) {
|
||||
payload.bootstrap = fs.readFileSync(args.bootstrap, 'utf-8');
|
||||
} else {
|
||||
payload.bootstrap = args.bootstrap;
|
||||
payload.bootstrap = args.bootstrap;
|
||||
}
|
||||
if (args.bootstrapFile) {
|
||||
if (!fs.existsSync(args.bootstrapFile)) {
|
||||
console.error(`${RED}Error: Bootstrap file not found: ${args.bootstrapFile}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
payload.bootstrap_content = fs.readFileSync(args.bootstrapFile, 'utf-8');
|
||||
}
|
||||
// Add input files
|
||||
if (args.files && args.files.length > 0) {
|
||||
payload.input_files = args.files.map(filepath => {
|
||||
try {
|
||||
const content = fs.readFileSync(filepath);
|
||||
return {
|
||||
filename: path.basename(filepath),
|
||||
content_base64: content.toString('base64')
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`${RED}Error: Input file not found: ${filepath}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (args.network) payload.network = args.network;
|
||||
if (args.vcpu) payload.vcpu = args.vcpu;
|
||||
|
|
@ -579,6 +614,7 @@ function parseArgs(argv: string[]): Args {
|
|||
domains: null,
|
||||
type: null,
|
||||
bootstrap: null,
|
||||
bootstrapFile: null,
|
||||
info: null,
|
||||
logs: null,
|
||||
tail: null,
|
||||
|
|
@ -656,6 +692,9 @@ function parseArgs(argv: string[]): Args {
|
|||
} else if (arg === '--bootstrap' && i + 1 < argv.length) {
|
||||
args.bootstrap = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--bootstrap-file' && i + 1 < argv.length) {
|
||||
args.bootstrapFile = argv[++i];
|
||||
i++;
|
||||
} else if (arg === '--info' && i + 1 < argv.length) {
|
||||
args.info = argv[++i];
|
||||
i++;
|
||||
|
|
@ -744,7 +783,8 @@ Service options:
|
|||
--ports PORTS Comma-separated ports
|
||||
--domains DOMAINS Custom domains
|
||||
--type TYPE Service type (minecraft|mumble|teamspeak|source|tcp|udp)
|
||||
--bootstrap CMD Bootstrap command/file
|
||||
--bootstrap CMD Bootstrap command or URI
|
||||
--bootstrap-file FILE Upload local file as bootstrap script
|
||||
-l, --list List services
|
||||
--info ID Get service details
|
||||
--logs ID Get all logs
|
||||
|
|
|
|||
73
un.v
73
un.v
|
|
@ -95,6 +95,25 @@ fn escape_json(s string) string {
|
|||
return result
|
||||
}
|
||||
|
||||
fn base64_encode_file(filename string) string {
|
||||
cmd := "base64 -w0 '${filename}'"
|
||||
result := os.execute(cmd)
|
||||
return result.output.trim_space()
|
||||
}
|
||||
|
||||
fn build_input_files_json(files []string) string {
|
||||
if files.len == 0 {
|
||||
return ''
|
||||
}
|
||||
mut entries := []string{}
|
||||
for f in files {
|
||||
basename := os.file_name(f)
|
||||
content := base64_encode_file(f)
|
||||
entries << '{"filename":"${basename}","content":"${content}"}'
|
||||
}
|
||||
return ',"input_files":[' + entries.join(',') + ']'
|
||||
}
|
||||
|
||||
fn exec_curl(cmd string) string {
|
||||
result := os.execute(cmd)
|
||||
output := result.output
|
||||
|
|
@ -252,7 +271,7 @@ fn cmd_execute(source_file string, envs []string, artifacts bool, network string
|
|||
println(exec_curl(cmd))
|
||||
}
|
||||
|
||||
fn cmd_session(list bool, kill string, shell string, network string, vcpu int, tmux bool, screen bool, api_key string) {
|
||||
fn cmd_session(list bool, kill string, shell string, network string, vcpu int, tmux bool, screen bool, input_files []string, api_key string) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
|
|
@ -283,6 +302,7 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
|
|||
if screen {
|
||||
json += ',"persistence":"screen"'
|
||||
}
|
||||
json += build_input_files_json(input_files)
|
||||
json += '}'
|
||||
|
||||
println('${yellow}Creating session...${reset}')
|
||||
|
|
@ -290,7 +310,7 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
|
|||
println(exec_curl(cmd))
|
||||
}
|
||||
|
||||
fn cmd_service(name string, ports string, service_type string, bootstrap string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, api_key string) {
|
||||
fn cmd_service(name string, ports string, service_type string, bootstrap string, bootstrap_file string, list bool, info string, logs string, tail string, sleep string, wake string, destroy string, execute string, command string, dump_bootstrap string, dump_file string, network string, vcpu int, input_files []string, api_key string) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
|
|
@ -389,11 +409,18 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
|
|||
json += ',"service_type":"${service_type}"'
|
||||
}
|
||||
if bootstrap != '' {
|
||||
if os.exists(bootstrap) {
|
||||
boot_code := os.read_file(bootstrap) or { bootstrap }
|
||||
json += ',"bootstrap":"${escape_json(boot_code)}"'
|
||||
json += ',"bootstrap":"${escape_json(bootstrap)}"'
|
||||
}
|
||||
if bootstrap_file != '' {
|
||||
if os.exists(bootstrap_file) {
|
||||
boot_code := os.read_file(bootstrap_file) or {
|
||||
eprintln('${red}Error: Could not read bootstrap file: ${bootstrap_file}${reset}')
|
||||
exit(1)
|
||||
}
|
||||
json += ',"bootstrap_content":"${escape_json(boot_code)}"'
|
||||
} else {
|
||||
json += ',"bootstrap":"${escape_json(bootstrap)}"'
|
||||
eprintln('${red}Error: Bootstrap file not found: ${bootstrap_file}${reset}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
if network != '' {
|
||||
|
|
@ -402,6 +429,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
|
|||
if vcpu > 0 {
|
||||
json += ',"vcpu":${vcpu}'
|
||||
}
|
||||
json += build_input_files_json(input_files)
|
||||
json += '}'
|
||||
|
||||
println('${yellow}Creating service...${reset}')
|
||||
|
|
@ -459,6 +487,7 @@ fn main() {
|
|||
mut tmux := false
|
||||
mut screen := false
|
||||
|
||||
mut input_files := []string{}
|
||||
mut i := 2
|
||||
for i < os.args.len {
|
||||
match os.args[i] {
|
||||
|
|
@ -485,12 +514,22 @@ fn main() {
|
|||
i++
|
||||
api_key = os.args[i]
|
||||
}
|
||||
'-f' {
|
||||
i++
|
||||
f := os.args[i]
|
||||
if os.exists(f) {
|
||||
input_files << f
|
||||
} else {
|
||||
eprintln('Error: File not found: ${f}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, api_key)
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, input_files, api_key)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -499,6 +538,7 @@ fn main() {
|
|||
mut ports := ''
|
||||
mut service_type := ''
|
||||
mut bootstrap := ''
|
||||
mut bootstrap_file := ''
|
||||
mut list := false
|
||||
mut info := ''
|
||||
mut logs := ''
|
||||
|
|
@ -512,6 +552,7 @@ fn main() {
|
|||
mut dump_file := ''
|
||||
mut network := ''
|
||||
mut vcpu := 0
|
||||
mut input_files := []string{}
|
||||
|
||||
mut i := 2
|
||||
for i < os.args.len {
|
||||
|
|
@ -532,6 +573,10 @@ fn main() {
|
|||
i++
|
||||
bootstrap = os.args[i]
|
||||
}
|
||||
'--bootstrap-file' {
|
||||
i++
|
||||
bootstrap_file = os.args[i]
|
||||
}
|
||||
'--list' { list = true }
|
||||
'--info' {
|
||||
i++
|
||||
|
|
@ -585,13 +630,23 @@ fn main() {
|
|||
i++
|
||||
api_key = os.args[i]
|
||||
}
|
||||
'-f' {
|
||||
i++
|
||||
f := os.args[i]
|
||||
if os.exists(f) {
|
||||
input_files << f
|
||||
} else {
|
||||
eprintln('Error: File not found: ${f}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
else {}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
cmd_service(name, ports, service_type, bootstrap, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network,
|
||||
vcpu, api_key)
|
||||
cmd_service(name, ports, service_type, bootstrap, bootstrap_file, list, info, logs, tail, sleep, wake, destroy, execute, command, dump_bootstrap, dump_file, network,
|
||||
vcpu, input_files, api_key)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
124
un.zig
124
un.zig
|
|
@ -101,6 +101,55 @@ fn buildAuthCmd(allocator: std.mem.Allocator, method: []const u8, path: []const
|
|||
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}' -H 'X-Timestamp: {s}' -H 'X-Signature: {s}'", .{ public_key, timestamp_str, signature });
|
||||
}
|
||||
|
||||
fn base64EncodeFile(allocator: std.mem.Allocator, filename: []const u8) ![]u8 {
|
||||
const cmd = try std.fmt.allocPrint(allocator, "base64 -w0 '{s}'", .{filename});
|
||||
defer allocator.free(cmd);
|
||||
|
||||
const result = std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = &[_][]const u8{ "sh", "-c", cmd },
|
||||
}) catch return try allocator.dupe(u8, "");
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
|
||||
return try allocator.dupe(u8, trimmed);
|
||||
}
|
||||
|
||||
fn buildInputFilesJson(allocator: std.mem.Allocator, files: std.ArrayList([]const u8)) ![]u8 {
|
||||
if (files.items.len == 0) {
|
||||
return try allocator.dupe(u8, "");
|
||||
}
|
||||
|
||||
var list = std.ArrayList(u8).init(allocator);
|
||||
defer list.deinit();
|
||||
|
||||
try list.appendSlice(",\"input_files\":[");
|
||||
|
||||
for (files.items, 0..) |file, i| {
|
||||
if (i > 0) try list.append(',');
|
||||
|
||||
// Get basename
|
||||
var basename: []const u8 = file;
|
||||
if (mem.lastIndexOfScalar(u8, file, '/')) |idx| {
|
||||
basename = file[idx + 1 ..];
|
||||
}
|
||||
|
||||
// Base64 encode file content
|
||||
const content = try base64EncodeFile(allocator, file);
|
||||
defer allocator.free(content);
|
||||
|
||||
const entry = try std.fmt.allocPrint(allocator, "{{\"filename\":\"{s}\",\"content\":\"{s}\"}}", .{ basename, content });
|
||||
defer allocator.free(entry);
|
||||
|
||||
try list.appendSlice(entry);
|
||||
}
|
||||
|
||||
try list.append(']');
|
||||
|
||||
return list.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn main() !u8 {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
|
|
@ -133,6 +182,8 @@ pub fn main() !u8 {
|
|||
var list = false;
|
||||
var kill: ?[]const u8 = null;
|
||||
var shell: ?[]const u8 = null;
|
||||
var input_files = std.ArrayList([]const u8).init(allocator);
|
||||
defer input_files.deinit();
|
||||
var i: usize = 2;
|
||||
while (i < args.len) : (i += 1) {
|
||||
if (mem.eql(u8, args[i], "--list")) {
|
||||
|
|
@ -147,6 +198,15 @@ pub fn main() !u8 {
|
|||
i += 1;
|
||||
allocator.free(public_key);
|
||||
public_key = try allocator.dupe(u8, args[i]);
|
||||
} else if (mem.eql(u8, args[i], "-f") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
const file = args[i];
|
||||
// Check if file exists
|
||||
fs.cwd().access(file, .{}) catch {
|
||||
std.debug.print("Error: File not found: {s}\n", .{file});
|
||||
return 1;
|
||||
};
|
||||
try input_files.append(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +228,9 @@ pub fn main() !u8 {
|
|||
std.debug.print("\x1b[32mSession terminated: {s}\x1b[0m\n", .{k});
|
||||
} else {
|
||||
const sh = shell orelse "bash";
|
||||
const json = try std.fmt.allocPrint(allocator, "{{\"shell\":\"{s}\"}}", .{sh});
|
||||
const input_files_json = try buildInputFilesJson(allocator, input_files);
|
||||
defer allocator.free(input_files_json);
|
||||
const json = try std.fmt.allocPrint(allocator, "{{\"shell\":\"{s}\"{s}}}", .{ sh, input_files_json });
|
||||
defer allocator.free(json);
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", "/sessions", json, public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
|
|
@ -187,11 +249,15 @@ pub fn main() !u8 {
|
|||
var name: ?[]const u8 = null;
|
||||
var ports: ?[]const u8 = null;
|
||||
var service_type: ?[]const u8 = null;
|
||||
var bootstrap: ?[]const u8 = null;
|
||||
var bootstrap_file: ?[]const u8 = null;
|
||||
var info: ?[]const u8 = null;
|
||||
var execute: ?[]const u8 = null;
|
||||
var command: ?[]const u8 = null;
|
||||
var dump_bootstrap: ?[]const u8 = null;
|
||||
var dump_file: ?[]const u8 = null;
|
||||
var input_files = std.ArrayList([]const u8).init(allocator);
|
||||
defer input_files.deinit();
|
||||
var i: usize = 2;
|
||||
while (i < args.len) : (i += 1) {
|
||||
if (mem.eql(u8, args[i], "--list")) {
|
||||
|
|
@ -205,6 +271,12 @@ pub fn main() !u8 {
|
|||
} else if (mem.eql(u8, args[i], "--type") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
service_type = args[i];
|
||||
} else if (mem.eql(u8, args[i], "--bootstrap") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
bootstrap = args[i];
|
||||
} else if (mem.eql(u8, args[i], "--bootstrap-file") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
bootstrap_file = args[i];
|
||||
} else if (mem.eql(u8, args[i], "--info") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
info = args[i];
|
||||
|
|
@ -224,6 +296,15 @@ pub fn main() !u8 {
|
|||
i += 1;
|
||||
allocator.free(public_key);
|
||||
public_key = try allocator.dupe(u8, args[i]);
|
||||
} else if (mem.eql(u8, args[i], "-f") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
const file = args[i];
|
||||
// Check if file exists
|
||||
fs.cwd().access(file, .{}) catch {
|
||||
std.debug.print("Error: File not found: {s}\n", .{file});
|
||||
return 1;
|
||||
};
|
||||
try input_files.append(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +387,7 @@ pub fn main() !u8 {
|
|||
return 1;
|
||||
}
|
||||
} else if (name) |n| {
|
||||
var json_buf: [4096]u8 = undefined;
|
||||
var json_buf: [65536]u8 = undefined;
|
||||
var json_stream = std.io.fixedBufferStream(&json_buf);
|
||||
const writer = json_stream.writer();
|
||||
try writer.print("{{\"name\":\"{s}\"", .{n});
|
||||
|
|
@ -316,6 +397,45 @@ pub fn main() !u8 {
|
|||
if (service_type) |t| {
|
||||
try writer.print(",\"service_type\":\"{s}\"", .{t});
|
||||
}
|
||||
if (bootstrap) |b| {
|
||||
try writer.writeAll(",\"bootstrap\":\"");
|
||||
// Escape JSON
|
||||
for (b) |c| {
|
||||
switch (c) {
|
||||
'"' => try writer.writeAll("\\\""),
|
||||
'\\' => try writer.writeAll("\\\\"),
|
||||
'\n' => try writer.writeAll("\\n"),
|
||||
'\r' => try writer.writeAll("\\r"),
|
||||
'\t' => try writer.writeAll("\\t"),
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
try writer.writeAll("\"");
|
||||
}
|
||||
if (bootstrap_file) |bf| {
|
||||
const boot_content = fs.cwd().readFileAlloc(allocator, bf, 10 * 1024 * 1024) catch |err| {
|
||||
std.debug.print("\x1b[31mError: Bootstrap file not found: {s} ({})\x1b[0m\n", .{ bf, err });
|
||||
return 1;
|
||||
};
|
||||
defer allocator.free(boot_content);
|
||||
try writer.writeAll(",\"bootstrap_content\":\"");
|
||||
// Escape JSON
|
||||
for (boot_content) |c| {
|
||||
switch (c) {
|
||||
'"' => try writer.writeAll("\\\""),
|
||||
'\\' => try writer.writeAll("\\\\"),
|
||||
'\n' => try writer.writeAll("\\n"),
|
||||
'\r' => try writer.writeAll("\\r"),
|
||||
'\t' => try writer.writeAll("\\t"),
|
||||
else => try writer.writeByte(c),
|
||||
}
|
||||
}
|
||||
try writer.writeAll("\"");
|
||||
}
|
||||
// Add input_files JSON
|
||||
const input_files_json = try buildInputFilesJson(allocator, input_files);
|
||||
defer allocator.free(input_files_json);
|
||||
try writer.writeAll(input_files_json);
|
||||
try writer.writeAll("}");
|
||||
const json_str = json_stream.getWritten();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue