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:
Russell Ballestrini 2026-01-02 14:06:36 -05:00
parent a5a4f23594
commit c6813e5a18
40 changed files with 2506 additions and 329 deletions

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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""'"

86
un.cpp
View file

@ -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,13 +454,35 @@ 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()) {
struct stat st;
if (stat(bootstrap.c_str(), &st) == 0) {
string boot_code = read_file(bootstrap);
json << ",\"bootstrap\":\"" << escape_json(boot_code) << "\"";
} else {
json << ",\"bootstrap\":\"" << escape_json(bootstrap) << "\"";
}
if (!bootstrap_file.empty()) {
struct stat st;
if (stat(bootstrap_file.c_str(), &st) == 0) {
string boot_code = read_file(bootstrap_file);
json << ",\"bootstrap_content\":\"" << escape_json(boot_code) << "\"";
} else {
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;
@ -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;
}

66
un.cr
View file

@ -262,8 +262,36 @@ def cmd_session(args)
return
end
STDERR.puts "#{RED}Error: Use --list or --kill#{RESET}"
# 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,12 +497,17 @@ 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))
else
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
STDERR.puts "#{RED}Error: Bootstrap file not found: #{bootstrap_file}#{RESET}"
exit 1
end
end
# Add network
@ -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
View file

@ -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));
} else {
json ~= format(`,"bootstrap":"%s"`, escapeJson(bootstrap));
}
if (!bootstrapFile.empty) {
if (exists(bootstrapFile)) {
string bootCode = readText(bootstrapFile);
json ~= format(`,"bootstrap_content":"%s"`, escapeJson(bootCode));
} else {
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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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(0, '(A)') 'Error: Use --list, --info, --logs, --freeze, --unfreeze, --destroy, or --dump-bootstrap'
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, --dump-bootstrap, or --name NAME'
stop 1
end if
end subroutine handle_service

103
un.forth
View file

@ -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
View file

@ -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]

60
un.go
View file

@ -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
}
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":

View file

@ -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
View file

@ -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

76
un.jl
View file

@ -276,8 +276,36 @@ function cmd_session(args)
return
end
println(stderr, "$(RED)Error: Use --list or --kill$(RESET)")
# 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

46
un.js
View file

@ -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;
}
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
View file

@ -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
View file

@ -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))))

45
un.lua
View file

@ -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
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

65
un.m
View file

@ -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,13 +698,39 @@ void cmdService(NSArray* args) {
}
if (bootstrap) {
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:bootstrap]) {
NSString* content = [NSString stringWithContentsOfFile:bootstrap encoding:NSUTF8StringEncoding error:nil];
payload[@"bootstrap"] = content;
} else {
payload[@"bootstrap"] = bootstrap;
}
if (bootstrapFile) {
NSFileManager* fm = [NSFileManager defaultManager];
if ([fm fileExistsAtPath:bootstrapFile]) {
NSString* content = [NSString stringWithContentsOfFile:bootstrapFile encoding:NSUTF8StringEncoding error:nil];
payload[@"bootstrap_content"] = content;
} else {
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;

92
un.ml
View file

@ -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
View file

@ -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)}"""")
else:
json.add(fmt""","bootstrap":"{escapeJson(bootstrap)}"""")
if bootstrapFile != "":
if fileExists(bootstrapFile):
let bootCode = readFile(bootstrapFile)
json.add(fmt""","bootstrap_content":"{escapeJson(bootCode)}"""")
else:
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
cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey)
else: discard
inc i
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
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey)
else: discard
inc i
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

47
un.php
View file

@ -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'];
}
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

60
un.pl
View file

@ -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};
}
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
View file

@ -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
View file

@ -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

49
un.py
View file

@ -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
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")

65
un.r
View file

@ -281,8 +281,36 @@ cmd_session <- function(args) {
return()
}
cat(sprintf("%sError: Use --list or --kill%s\n", RED, RESET), file = stderr())
# 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,11 +513,16 @@ cmd_service <- function(args) {
}
if (!is.null(args$bootstrap)) {
if (file.exists(args$bootstrap)) {
payload$bootstrap <- paste(readLines(args$bootstrap, warn = FALSE), collapse = "\n")
} else {
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 {
cat(sprintf("%sError: Bootstrap file not found: %s%s\n", RED, args$bootstrap_file, RESET), file = stderr())
quit(status = 1)
}
}
if (!is.null(args$network)) {
@ -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
View file

@ -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;
} else {
%payload<bootstrap> = $bootstrap;
}
if $bootstrap-file {
if $bootstrap-file.IO.e && $bootstrap-file.IO.f {
%payload<bootstrap_content> = $bootstrap-file.IO.slurp;
} else {
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>}";

46
un.rb
View file

@ -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]
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
View file

@ -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
View file

@ -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))))

62
un.sh
View file

@ -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}')
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
View file

@ -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]"

48
un.ts
View file

@ -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;
}
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
View file

@ -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,12 +409,19 @@ 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)}"'
} else {
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 {
eprintln('${red}Error: Bootstrap file not found: ${bootstrap_file}${reset}')
exit(1)
}
}
if network != '' {
json += ',"network":"${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
View file

@ -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();