Add HMAC authentication to all 42 implementations
- Update all un.* implementations to use HMAC-SHA256 signing - Headers: Authorization (Bearer public_key), X-Timestamp, X-Signature - Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body") - Fix test suite issues (bash arithmetic, ES module compat, TCL shebang) - Add CLAUDE.md with inception testing documentation - Update README.md with HMAC auth and dependency table - All 38 implementations pass inception test via un2
This commit is contained in:
parent
dba8ffeedc
commit
a4f9bfa377
56 changed files with 3597 additions and 1312 deletions
123
CLAUDE.md
Normal file
123
CLAUDE.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Claude AI Instructions for un-inception
|
||||
|
||||
## Project Overview
|
||||
|
||||
UN CLI Inception - The UN CLI written in every language it can execute. 42 implementations, one unified interface.
|
||||
|
||||
## Authentication
|
||||
|
||||
**HMAC Authentication** (current):
|
||||
```bash
|
||||
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
|
||||
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
|
||||
```
|
||||
|
||||
The auth pattern for all implementations:
|
||||
- `Authorization: Bearer {public_key}`
|
||||
- `X-Timestamp: {unix_seconds}`
|
||||
- `X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")`
|
||||
|
||||
Legacy `UNSANDBOX_API_KEY` is still supported as fallback.
|
||||
|
||||
### HMAC Dependencies
|
||||
|
||||
Each implementation needs HMAC-SHA256 capability:
|
||||
|
||||
| Language | Dependency | Install |
|
||||
|----------|------------|---------|
|
||||
| Python | `hmac`, `hashlib` | Built-in |
|
||||
| JavaScript/TS | `crypto` | Built-in (Node.js) |
|
||||
| Ruby | `openssl` | Built-in |
|
||||
| Go | `crypto/hmac` | Built-in |
|
||||
| PHP | `hash_hmac()` | Built-in |
|
||||
| Perl | `Digest::SHA` | Core module |
|
||||
| Lua | `openssl` CLI | `apt install openssl` |
|
||||
| Bash | `openssl` CLI | `apt install openssl` |
|
||||
| Rust | `hmac`, `sha2` | `cargo add hmac sha2` |
|
||||
| C/C++ | OpenSSL | `apt install libssl-dev` + `-lssl -lcrypto` |
|
||||
| Java | `javax.crypto` | Built-in |
|
||||
| C# | `System.Security.Cryptography` | Built-in |
|
||||
| Haskell | `cryptonite` | `cabal install cryptonite` |
|
||||
| Clojure | `buddy-core` | Add to deps.edn |
|
||||
| Erlang/Elixir | `:crypto` | OTP built-in |
|
||||
| Julia | `SHA` | Built-in |
|
||||
| R | `openssl` | `install.packages("openssl")` |
|
||||
|
||||
**Note**: Languages without native HMAC (Lua, Bash, AWK, Forth) shell out to `openssl dgst -sha256 -hmac`.
|
||||
|
||||
## The Inception Matrix - Testing Languages Without Local Interpreters
|
||||
|
||||
**CRITICAL INSIGHT**: Use `un` (the C implementation) to run tests for languages not installed locally!
|
||||
|
||||
If a language isn't available on the local machine (e.g., PHP, Julia, Haskell), run the UN implementation through unsandbox itself:
|
||||
|
||||
```bash
|
||||
# Key flags:
|
||||
# -n semitrusted = allow network access so inner script can call API
|
||||
# -e KEY=VALUE = pass API keys to inner script
|
||||
|
||||
# Don't have PHP installed? Run un.php through unsandbox!
|
||||
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.php test/fib.py
|
||||
|
||||
# Don't have Julia? Run un.jl through unsandbox!
|
||||
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.jl test/fib.py
|
||||
```
|
||||
|
||||
This is the **inception** - using un to run un to run code. Each layer executes through unsandbox's remote execution API.
|
||||
|
||||
### Inception Test Matrix
|
||||
|
||||
To test ALL 42 implementations regardless of local interpreters:
|
||||
|
||||
```bash
|
||||
# Use un (C implementation) to test all others through unsandbox
|
||||
for impl in un.py un.js un.rb un.go un.php un.pl un.lua; do
|
||||
echo "Testing $impl..."
|
||||
un -n semitrusted \
|
||||
-e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY \
|
||||
-e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY \
|
||||
"$impl" test/fib.py
|
||||
done
|
||||
```
|
||||
|
||||
The test suite in `tests/run_all_tests.sh` currently skips languages without local interpreters. Use the inception pattern with `un` to achieve 100% test coverage.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `un.*` - 42 UN CLI implementations (un.py, un.js, un.rb, un.go, etc.)
|
||||
- `tests/` - Test suites for each implementation
|
||||
- `test/` - Shared test files (fib.py, fib.sh, etc.)
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Set auth
|
||||
export UNSANDBOX_PUBLIC_KEY="unsb-pk-zhi3-b6cv-jvqc-uven"
|
||||
export UNSANDBOX_SECRET_KEY="unsb-sk-z4a93-a33xy-7u7eh-pngpg"
|
||||
|
||||
# Run all available tests
|
||||
./tests/run_all_tests.sh
|
||||
|
||||
# Run individual test
|
||||
python3 tests/test_un_py.py
|
||||
lua tests/test_un_lua.lua
|
||||
bash tests/test_un_sh.sh
|
||||
```
|
||||
|
||||
## Common Test Fixes
|
||||
|
||||
### Bash arithmetic in `set -e` mode
|
||||
The pattern `((VAR++))` returns exit code 1 when VAR is 0. Use `VAR=$((VAR + 1))` instead.
|
||||
|
||||
### Script directory detection
|
||||
- **Lua**: `arg[0]:match("(.*/)") or "./"`
|
||||
- **TypeScript**: `path.dirname(process.argv[1] || __filename)`
|
||||
- **Bash**: `SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"`
|
||||
|
||||
### Shebang lines
|
||||
Shebang MUST be on line 1, not buried in license headers.
|
||||
|
||||
## Related Repos
|
||||
|
||||
- `~/git/unsandbox.com/` - Portal (contains un.c CLI at cli/un.c)
|
||||
- `~/git/api.unsandbox.com/` - API server
|
||||
68
README.md
68
README.md
|
|
@ -11,8 +11,9 @@ The UN CLI written in every language it can execute. **42 implementations, one u
|
|||
git clone https://github.com/russellballestrini/un-inception.git
|
||||
cd un-inception
|
||||
|
||||
# Set your API key
|
||||
export UNSANDBOX_API_KEY=your_key_here
|
||||
# Set your API keys (HMAC authentication)
|
||||
export UNSANDBOX_PUBLIC_KEY="unsb-pk-xxxx-xxxx-xxxx-xxxx"
|
||||
export UNSANDBOX_SECRET_KEY="unsb-sk-xxxxx-xxxxx-xxxxx-xxxxx"
|
||||
|
||||
# Run any implementation
|
||||
python3 un.py test/fib.py
|
||||
|
|
@ -24,6 +25,41 @@ go run un.go test/fib.py
|
|||
# fib(10) = 55
|
||||
```
|
||||
|
||||
## HMAC Authentication
|
||||
|
||||
All implementations use HMAC-SHA256 for request signing:
|
||||
|
||||
```
|
||||
Authorization: Bearer {public_key}
|
||||
X-Timestamp: {unix_seconds}
|
||||
X-Signature: HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
|
||||
```
|
||||
|
||||
### HMAC Dependencies by Language
|
||||
|
||||
| Language | HMAC Library | Notes |
|
||||
|----------|--------------|-------|
|
||||
| **Python** | `hmac`, `hashlib` | Standard library |
|
||||
| **JavaScript** | `crypto` | Node.js built-in |
|
||||
| **TypeScript** | `crypto` | Node.js built-in |
|
||||
| **Ruby** | `openssl` | Standard library |
|
||||
| **Go** | `crypto/hmac`, `crypto/sha256` | Standard library |
|
||||
| **Rust** | `hmac`, `sha2` | Crates (add to Cargo.toml) |
|
||||
| **PHP** | `hash_hmac()` | Built-in function |
|
||||
| **Perl** | `Digest::SHA` | Core module |
|
||||
| **Lua** | `openssl` CLI | Shells out to `openssl dgst` |
|
||||
| **Bash** | `openssl` CLI | Uses `openssl dgst -sha256 -hmac` |
|
||||
| **C/C++** | OpenSSL | Link with `-lssl -lcrypto` |
|
||||
| **Java** | `javax.crypto` | Standard library |
|
||||
| **C#** | `System.Security.Cryptography` | .NET built-in |
|
||||
| **Haskell** | `cryptonite` | Hackage package |
|
||||
| **Clojure** | `buddy-core` | Clojars dependency |
|
||||
| **Erlang/Elixir** | `:crypto` | OTP built-in |
|
||||
| **Julia** | `SHA` | Standard library |
|
||||
| **R** | `openssl` | CRAN package |
|
||||
|
||||
Most languages have HMAC-SHA256 in their standard library. Languages without native support (Lua, Bash, AWK) shell out to the `openssl` command-line tool.
|
||||
|
||||
## Implementations
|
||||
|
||||
| Language | File | Category |
|
||||
|
|
@ -121,9 +157,35 @@ Each implementation supports:
|
|||
# Output: fib(10) = 55
|
||||
|
||||
# Run the full test matrix (requires API key)
|
||||
./tests/run_matrix.sh
|
||||
./tests/run_all_tests.sh
|
||||
```
|
||||
|
||||
## The Inception Matrix
|
||||
|
||||
**Use un to run un inside un!** Don't have a language installed locally? Run its implementation through unsandbox using `un` (the C implementation):
|
||||
|
||||
```bash
|
||||
# Pass API keys via -e so the inner un.* can call the API
|
||||
# Use -n semitrusted so inner script can reach api.unsandbox.com
|
||||
|
||||
# Don't have PHP? Run un.php through unsandbox!
|
||||
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.php test/fib.py
|
||||
|
||||
# Don't have Julia? Run un.jl through unsandbox!
|
||||
un -n semitrusted -e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY -e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY un.jl test/fib.py
|
||||
|
||||
# Test ALL implementations regardless of local interpreters
|
||||
for impl in un.py un.js un.rb un.go un.php un.pl un.lua; do
|
||||
echo "Testing $impl..."
|
||||
un -n semitrusted \
|
||||
-e UNSANDBOX_PUBLIC_KEY=$UNSANDBOX_PUBLIC_KEY \
|
||||
-e UNSANDBOX_SECRET_KEY=$UNSANDBOX_SECRET_KEY \
|
||||
"$impl" test/fib.py
|
||||
done
|
||||
```
|
||||
|
||||
This is the **inception** - each layer executes through unsandbox's remote API, so you can test any implementation using `un` (the canonical C implementation) as the runner.
|
||||
|
||||
## Links
|
||||
|
||||
- [unsandbox.com](https://unsandbox.com) - Remote code execution API
|
||||
|
|
|
|||
94
Un.cs
94
Un.cs
|
|
@ -45,6 +45,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
class Un
|
||||
{
|
||||
|
|
@ -110,7 +111,7 @@ class Un
|
|||
|
||||
static void CmdExecute(Args args)
|
||||
{
|
||||
string apiKey = GetApiKey(args.ApiKey);
|
||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||
string code = File.ReadAllText(args.SourceFile);
|
||||
string language = DetectLanguage(args.SourceFile);
|
||||
|
||||
|
|
@ -165,7 +166,7 @@ class Un
|
|||
payload["vcpu"] = args.Vcpu;
|
||||
}
|
||||
|
||||
var result = ApiRequest("/execute", "POST", payload, apiKey);
|
||||
var result = ApiRequest("/execute", "POST", payload, publicKey, secretKey);
|
||||
|
||||
if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"]))
|
||||
{
|
||||
|
|
@ -197,11 +198,11 @@ class Un
|
|||
|
||||
static void CmdSession(Args args)
|
||||
{
|
||||
string apiKey = GetApiKey(args.ApiKey);
|
||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||
|
||||
if (args.SessionList)
|
||||
{
|
||||
var result = ApiRequest("/sessions", "GET", null, apiKey);
|
||||
var result = ApiRequest("/sessions", "GET", null, publicKey, secretKey);
|
||||
var sessions = result.ContainsKey("sessions") ? result["sessions"] as List<object> : null;
|
||||
if (sessions == null || sessions.Count == 0)
|
||||
{
|
||||
|
|
@ -224,7 +225,7 @@ class Un
|
|||
|
||||
if (args.SessionKill != null)
|
||||
{
|
||||
ApiRequest($"/sessions/{args.SessionKill}", "DELETE", null, apiKey);
|
||||
ApiRequest($"/sessions/{args.SessionKill}", "DELETE", null, publicKey, secretKey);
|
||||
Console.WriteLine($"{GREEN}Session terminated: {args.SessionKill}{RESET}");
|
||||
return;
|
||||
}
|
||||
|
|
@ -243,16 +244,16 @@ class Un
|
|||
}
|
||||
|
||||
Console.WriteLine($"{YELLOW}Creating session...{RESET}");
|
||||
var createResult = ApiRequest("/sessions", "POST", payload, apiKey);
|
||||
var createResult = ApiRequest("/sessions", "POST", payload, publicKey, secretKey);
|
||||
Console.WriteLine($"{GREEN}Session created: {createResult["id"]}{RESET}");
|
||||
Console.WriteLine($"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}");
|
||||
}
|
||||
|
||||
static void CmdKey(Args args)
|
||||
{
|
||||
string apiKey = GetApiKey(args.ApiKey);
|
||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||
|
||||
var result = ApiRequest("/keys/validate", "POST", null, apiKey);
|
||||
var result = ApiRequest("/keys/validate", "POST", null, publicKey, secretKey);
|
||||
|
||||
if (!result.ContainsKey("valid"))
|
||||
{
|
||||
|
|
@ -335,11 +336,11 @@ class Un
|
|||
|
||||
static void CmdService(Args args)
|
||||
{
|
||||
string apiKey = GetApiKey(args.ApiKey);
|
||||
var (publicKey, secretKey) = GetApiKeys(args.ApiKey);
|
||||
|
||||
if (args.ServiceList)
|
||||
{
|
||||
var result = ApiRequest("/services", "GET", null, apiKey);
|
||||
var result = ApiRequest("/services", "GET", null, publicKey, secretKey);
|
||||
var services = result.ContainsKey("services") ? result["services"] as List<object> : null;
|
||||
if (services == null || services.Count == 0)
|
||||
{
|
||||
|
|
@ -366,42 +367,42 @@ class Un
|
|||
|
||||
if (args.ServiceInfo != null)
|
||||
{
|
||||
var result = ApiRequest($"/services/{args.ServiceInfo}", "GET", null, apiKey);
|
||||
var result = ApiRequest($"/services/{args.ServiceInfo}", "GET", null, publicKey, secretKey);
|
||||
Console.WriteLine(ToJson(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ServiceLogs != null)
|
||||
{
|
||||
var result = ApiRequest($"/services/{args.ServiceLogs}/logs", "GET", null, apiKey);
|
||||
var result = ApiRequest($"/services/{args.ServiceLogs}/logs", "GET", null, publicKey, secretKey);
|
||||
Console.WriteLine(result.ContainsKey("logs") ? result["logs"] : "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ServiceTail != null)
|
||||
{
|
||||
var result = ApiRequest($"/services/{args.ServiceTail}/logs?lines=9000", "GET", null, apiKey);
|
||||
var result = ApiRequest($"/services/{args.ServiceTail}/logs?lines=9000", "GET", null, publicKey, secretKey);
|
||||
Console.WriteLine(result.ContainsKey("logs") ? result["logs"] : "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ServiceSleep != null)
|
||||
{
|
||||
ApiRequest($"/services/{args.ServiceSleep}/sleep", "POST", null, apiKey);
|
||||
ApiRequest($"/services/{args.ServiceSleep}/sleep", "POST", null, publicKey, secretKey);
|
||||
Console.WriteLine($"{GREEN}Service sleeping: {args.ServiceSleep}{RESET}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ServiceWake != null)
|
||||
{
|
||||
ApiRequest($"/services/{args.ServiceWake}/wake", "POST", null, apiKey);
|
||||
ApiRequest($"/services/{args.ServiceWake}/wake", "POST", null, publicKey, secretKey);
|
||||
Console.WriteLine($"{GREEN}Service waking: {args.ServiceWake}{RESET}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.ServiceDestroy != null)
|
||||
{
|
||||
ApiRequest($"/services/{args.ServiceDestroy}", "DELETE", null, apiKey);
|
||||
ApiRequest($"/services/{args.ServiceDestroy}", "DELETE", null, publicKey, secretKey);
|
||||
Console.WriteLine($"{GREEN}Service destroyed: {args.ServiceDestroy}{RESET}");
|
||||
return;
|
||||
}
|
||||
|
|
@ -412,7 +413,7 @@ class Un
|
|||
{
|
||||
["command"] = args.ServiceCommand
|
||||
};
|
||||
var result = ApiRequest($"/services/{args.ServiceExecute}/execute", "POST", payload, apiKey);
|
||||
var result = ApiRequest($"/services/{args.ServiceExecute}/execute", "POST", payload, publicKey, secretKey);
|
||||
if (result.ContainsKey("stdout") && !string.IsNullOrEmpty((string)result["stdout"]))
|
||||
{
|
||||
Console.Write($"{BLUE}{result["stdout"]}{RESET}");
|
||||
|
|
@ -431,7 +432,7 @@ class Un
|
|||
{
|
||||
["command"] = "cat /tmp/bootstrap.sh"
|
||||
};
|
||||
var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", "POST", payload, apiKey);
|
||||
var result = ApiRequest($"/services/{args.ServiceDumpBootstrap}/execute", "POST", payload, publicKey, secretKey);
|
||||
|
||||
var bootstrap = result.ContainsKey("stdout") ? (string)result["stdout"] : null;
|
||||
if (!string.IsNullOrEmpty(bootstrap))
|
||||
|
|
@ -494,7 +495,7 @@ class Un
|
|||
payload["vcpu"] = args.Vcpu;
|
||||
}
|
||||
|
||||
var result = ApiRequest("/services", "POST", payload, apiKey);
|
||||
var result = ApiRequest("/services", "POST", payload, publicKey, secretKey);
|
||||
Console.WriteLine($"{GREEN}Service created: {result["id"]}{RESET}");
|
||||
Console.WriteLine($"Name: {result["name"]}");
|
||||
if (result.ContainsKey("url"))
|
||||
|
|
@ -508,15 +509,24 @@ class Un
|
|||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
static string GetApiKey(string argsKey)
|
||||
static (string, string) GetApiKeys(string argsKey)
|
||||
{
|
||||
string key = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
|
||||
if (string.IsNullOrEmpty(key))
|
||||
string publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY");
|
||||
string secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_API_KEY not set{RESET}");
|
||||
Environment.Exit(1);
|
||||
string legacyKey = argsKey ?? Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY");
|
||||
if (string.IsNullOrEmpty(legacyKey))
|
||||
{
|
||||
Console.Error.WriteLine($"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
return (legacyKey, null);
|
||||
}
|
||||
return key;
|
||||
|
||||
return (publicKey, secretKey);
|
||||
}
|
||||
|
||||
static string DetectLanguage(string filename)
|
||||
|
|
@ -534,20 +544,46 @@ class Un
|
|||
return ExtMap[ext];
|
||||
}
|
||||
|
||||
static Dictionary<string, object> ApiRequest(string endpoint, string method, Dictionary<string, object> data, string apiKey)
|
||||
static Dictionary<string, object> ApiRequest(string endpoint, string method, Dictionary<string, object> data, string publicKey, string secretKey)
|
||||
{
|
||||
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
|
||||
|
||||
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(API_BASE + endpoint);
|
||||
request.Method = method;
|
||||
request.Headers.Add("Authorization", $"Bearer {apiKey}");
|
||||
request.ContentType = "application/json";
|
||||
request.Timeout = 300000;
|
||||
|
||||
string body = "";
|
||||
if (data != null)
|
||||
{
|
||||
string json = ToJson(data);
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(json);
|
||||
body = ToJson(data);
|
||||
}
|
||||
|
||||
// Add HMAC authentication headers if secretKey is provided
|
||||
if (!string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
string message = $"{timestamp}:{method}:{endpoint}:{body}";
|
||||
|
||||
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
|
||||
{
|
||||
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
|
||||
string signature = BitConverter.ToString(hash).Replace("-", "").ToLower();
|
||||
|
||||
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
||||
request.Headers.Add("X-Timestamp", timestamp.ToString());
|
||||
request.Headers.Add("X-Signature", signature);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Legacy API key authentication
|
||||
request.Headers.Add("Authorization", $"Bearer {publicKey}");
|
||||
}
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(body);
|
||||
request.ContentLength = bytes.Length;
|
||||
using (Stream stream = request.GetRequestStream())
|
||||
{
|
||||
|
|
|
|||
136
Un.java
136
Un.java
|
|
@ -45,6 +45,8 @@ import java.net.*;
|
|||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Un {
|
||||
private static final String API_BASE = "https://api.unsandbox.com";
|
||||
|
|
@ -94,7 +96,9 @@ public class Un {
|
|||
}
|
||||
|
||||
private static void cmdExecute(Args args) throws Exception {
|
||||
String apiKey = getApiKey(args.apiKey);
|
||||
String[] keys = getApiKeys(args.apiKey);
|
||||
String publicKey = keys[0];
|
||||
String secretKey = keys[1];
|
||||
String code = Files.readString(Paths.get(args.sourceFile));
|
||||
String language = detectLanguage(args.sourceFile);
|
||||
|
||||
|
|
@ -137,7 +141,7 @@ public class Un {
|
|||
payload.put("vcpu", args.vcpu);
|
||||
}
|
||||
|
||||
Map<String, Object> result = apiRequest("/execute", "POST", payload, apiKey);
|
||||
Map<String, Object> result = apiRequest("/execute", "POST", payload, publicKey, secretKey);
|
||||
|
||||
String stdout = (String) result.get("stdout");
|
||||
String stderr = (String) result.get("stderr");
|
||||
|
|
@ -168,10 +172,12 @@ public class Un {
|
|||
}
|
||||
|
||||
private static void cmdSession(Args args) throws Exception {
|
||||
String apiKey = getApiKey(args.apiKey);
|
||||
String[] keys = getApiKeys(args.apiKey);
|
||||
String publicKey = keys[0];
|
||||
String secretKey = keys[1];
|
||||
|
||||
if (args.sessionList) {
|
||||
Map<String, Object> result = apiRequest("/sessions", "GET", null, apiKey);
|
||||
Map<String, Object> result = apiRequest("/sessions", "GET", null, publicKey, secretKey);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> sessions = (List<Map<String, Object>>) result.get("sessions");
|
||||
if (sessions == null || sessions.isEmpty()) {
|
||||
|
|
@ -190,7 +196,7 @@ public class Un {
|
|||
}
|
||||
|
||||
if (args.sessionKill != null) {
|
||||
apiRequest("/sessions/" + args.sessionKill, "DELETE", null, apiKey);
|
||||
apiRequest("/sessions/" + args.sessionKill, "DELETE", null, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Session terminated: " + args.sessionKill + RESET);
|
||||
return;
|
||||
}
|
||||
|
|
@ -205,16 +211,18 @@ public class Un {
|
|||
}
|
||||
|
||||
System.out.println(YELLOW + "Creating session..." + RESET);
|
||||
Map<String, Object> result = apiRequest("/sessions", "POST", payload, apiKey);
|
||||
Map<String, Object> result = apiRequest("/sessions", "POST", payload, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Session created: " + result.getOrDefault("id", "N/A") + RESET);
|
||||
System.out.println(YELLOW + "(Interactive sessions require WebSocket - use un2 for full support)" + RESET);
|
||||
}
|
||||
|
||||
private static void cmdService(Args args) throws Exception {
|
||||
String apiKey = getApiKey(args.apiKey);
|
||||
String[] keys = getApiKeys(args.apiKey);
|
||||
String publicKey = keys[0];
|
||||
String secretKey = keys[1];
|
||||
|
||||
if (args.serviceList) {
|
||||
Map<String, Object> result = apiRequest("/services", "GET", null, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services", "GET", null, publicKey, secretKey);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> services = (List<Map<String, Object>>) result.get("services");
|
||||
if (services == null || services.isEmpty()) {
|
||||
|
|
@ -239,37 +247,37 @@ public class Un {
|
|||
}
|
||||
|
||||
if (args.serviceInfo != null) {
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceInfo, "GET", null, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceInfo, "GET", null, publicKey, secretKey);
|
||||
System.out.println(toJson(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceLogs != null) {
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceLogs + "/logs", "GET", null, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceLogs + "/logs", "GET", null, publicKey, secretKey);
|
||||
System.out.println(result.getOrDefault("logs", ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceTail != null) {
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceTail + "/logs?lines=9000", "GET", null, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceTail + "/logs?lines=9000", "GET", null, publicKey, secretKey);
|
||||
System.out.println(result.getOrDefault("logs", ""));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceSleep != null) {
|
||||
apiRequest("/services/" + args.serviceSleep + "/sleep", "POST", null, apiKey);
|
||||
apiRequest("/services/" + args.serviceSleep + "/sleep", "POST", null, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Service sleeping: " + args.serviceSleep + RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceWake != null) {
|
||||
apiRequest("/services/" + args.serviceWake + "/wake", "POST", null, apiKey);
|
||||
apiRequest("/services/" + args.serviceWake + "/wake", "POST", null, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Service waking: " + args.serviceWake + RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceDestroy != null) {
|
||||
apiRequest("/services/" + args.serviceDestroy, "DELETE", null, apiKey);
|
||||
apiRequest("/services/" + args.serviceDestroy, "DELETE", null, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Service destroyed: " + args.serviceDestroy + RESET);
|
||||
return;
|
||||
}
|
||||
|
|
@ -277,7 +285,7 @@ public class Un {
|
|||
if (args.serviceExecute != null) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("command", args.serviceCommand);
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceExecute + "/execute", "POST", payload, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceExecute + "/execute", "POST", payload, publicKey, secretKey);
|
||||
String stdout = (String) result.get("stdout");
|
||||
String stderr = (String) result.get("stderr");
|
||||
if (stdout != null && !stdout.isEmpty()) {
|
||||
|
|
@ -293,7 +301,7 @@ public class Un {
|
|||
System.err.println("Fetching bootstrap script from " + args.serviceDumpBootstrap + "...");
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("command", "cat /tmp/bootstrap.sh");
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceDumpBootstrap + "/execute", "POST", payload, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services/" + args.serviceDumpBootstrap + "/execute", "POST", payload, publicKey, secretKey);
|
||||
|
||||
String bootstrap = (String) result.get("stdout");
|
||||
if (bootstrap != null && !bootstrap.isEmpty()) {
|
||||
|
|
@ -340,7 +348,7 @@ public class Un {
|
|||
payload.put("vcpu", args.vcpu);
|
||||
}
|
||||
|
||||
Map<String, Object> result = apiRequest("/services", "POST", payload, apiKey);
|
||||
Map<String, Object> result = apiRequest("/services", "POST", payload, publicKey, secretKey);
|
||||
System.out.println(GREEN + "Service created: " + result.getOrDefault("id", "N/A") + RESET);
|
||||
System.out.println("Name: " + result.getOrDefault("name", "N/A"));
|
||||
if (result.containsKey("url")) {
|
||||
|
|
@ -354,18 +362,20 @@ public class Un {
|
|||
}
|
||||
|
||||
private static void cmdKey(Args args) throws Exception {
|
||||
String apiKey = getApiKey(args.apiKey);
|
||||
String[] keys = getApiKeys(args.apiKey);
|
||||
String publicKey = keys[0];
|
||||
String secretKey = keys[1];
|
||||
|
||||
if (args.keyExtend) {
|
||||
// First validate to get public_key
|
||||
Map<String, Object> result = validateKey(apiKey);
|
||||
String publicKey = (String) result.get("public_key");
|
||||
if (publicKey == null || publicKey.isEmpty()) {
|
||||
Map<String, Object> result = validateKey(publicKey, secretKey);
|
||||
String pubKey = (String) result.get("public_key");
|
||||
if (pubKey == null || pubKey.isEmpty()) {
|
||||
System.err.println(RED + "Error: Could not retrieve public key" + RESET);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
String extendUrl = PORTAL_BASE + "/keys/extend?pk=" + urlEncode(publicKey);
|
||||
String extendUrl = PORTAL_BASE + "/keys/extend?pk=" + urlEncode(pubKey);
|
||||
System.out.println(YELLOW + "Opening browser to extend key:" + RESET);
|
||||
System.out.println(extendUrl);
|
||||
|
||||
|
|
@ -386,9 +396,9 @@ public class Un {
|
|||
}
|
||||
|
||||
// Default: validate key
|
||||
Map<String, Object> result = validateKey(apiKey);
|
||||
Map<String, Object> result = validateKey(publicKey, secretKey);
|
||||
Boolean expired = (Boolean) result.get("expired");
|
||||
String publicKey = (String) result.get("public_key");
|
||||
String pubKey = (String) result.get("public_key");
|
||||
String tier = (String) result.get("tier");
|
||||
String status = (String) result.get("status");
|
||||
String expiresAt = (String) result.get("expires_at");
|
||||
|
|
@ -399,7 +409,7 @@ public class Un {
|
|||
|
||||
if (expired != null && expired) {
|
||||
System.out.println(RED + "Expired" + RESET);
|
||||
System.out.println("Public Key: " + (publicKey != null ? publicKey : "N/A"));
|
||||
System.out.println("Public Key: " + (pubKey != null ? pubKey : "N/A"));
|
||||
System.out.println("Tier: " + (tier != null ? tier : "N/A"));
|
||||
System.out.println("Expired: " + (expiresAt != null ? expiresAt : "N/A"));
|
||||
System.out.println(YELLOW + "To renew: Visit " + PORTAL_BASE + "/keys/extend" + RESET);
|
||||
|
|
@ -408,7 +418,7 @@ public class Un {
|
|||
|
||||
// Valid key
|
||||
System.out.println(GREEN + "Valid" + RESET);
|
||||
System.out.println("Public Key: " + (publicKey != null ? publicKey : "N/A"));
|
||||
System.out.println("Public Key: " + (pubKey != null ? pubKey : "N/A"));
|
||||
System.out.println("Tier: " + (tier != null ? tier : "N/A"));
|
||||
System.out.println("Status: " + (status != null ? status : "N/A"));
|
||||
System.out.println("Expires: " + (expiresAt != null ? expiresAt : "N/A"));
|
||||
|
|
@ -418,11 +428,20 @@ public class Un {
|
|||
System.out.println("Concurrency: " + (concurrency != null ? concurrency : "N/A"));
|
||||
}
|
||||
|
||||
private static Map<String, Object> validateKey(String apiKey) throws Exception {
|
||||
URL url = new URL(PORTAL_BASE + "/keys/validate");
|
||||
private static Map<String, Object> validateKey(String publicKey, String secretKey) throws Exception {
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
String method = "POST";
|
||||
String path = "/keys/validate";
|
||||
String body = "";
|
||||
String signatureData = timestamp + ":" + method + ":" + path + ":" + body;
|
||||
String signature = hmacSha256(secretKey, signatureData);
|
||||
|
||||
URL url = new URL(PORTAL_BASE + path);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestMethod(method);
|
||||
conn.setRequestProperty("Authorization", "Bearer " + (publicKey != null ? publicKey : secretKey));
|
||||
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
|
||||
conn.setRequestProperty("X-Signature", signature);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(30000);
|
||||
|
|
@ -455,13 +474,46 @@ public class Un {
|
|||
}
|
||||
}
|
||||
|
||||
private static String getApiKey(String argsKey) {
|
||||
String key = argsKey != null ? argsKey : System.getenv("UNSANDBOX_API_KEY");
|
||||
if (key == null || key.isEmpty()) {
|
||||
System.err.println(RED + "Error: UNSANDBOX_API_KEY not set" + RESET);
|
||||
private static String[] getApiKeys(String argsKey) {
|
||||
String publicKey = null;
|
||||
String secretKey = null;
|
||||
|
||||
if (argsKey != null) {
|
||||
// If API key provided via args, use it as secret key for backwards compat
|
||||
secretKey = argsKey;
|
||||
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
} else {
|
||||
// Try new-style auth first
|
||||
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
secretKey = System.getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
// Fall back to old-style auth
|
||||
if (publicKey == null || secretKey == null) {
|
||||
String apiKey = System.getenv("UNSANDBOX_API_KEY");
|
||||
if (apiKey != null && !apiKey.isEmpty()) {
|
||||
secretKey = apiKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (secretKey == null || secretKey.isEmpty()) {
|
||||
System.err.println(RED + "Error: UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set" + RESET);
|
||||
System.exit(1);
|
||||
}
|
||||
return key;
|
||||
|
||||
return new String[] { publicKey, secretKey };
|
||||
}
|
||||
|
||||
private static String hmacSha256(String secretKey, String data) throws Exception {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256");
|
||||
mac.init(keySpec);
|
||||
byte[] hash = mac.doFinal(data.getBytes("UTF-8"));
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
hex.append(String.format("%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
private static String detectLanguage(String filename) throws Exception {
|
||||
|
|
@ -477,20 +529,26 @@ public class Un {
|
|||
return lang;
|
||||
}
|
||||
|
||||
private static Map<String, Object> apiRequest(String endpoint, String method, Map<String, Object> data, String apiKey) throws Exception {
|
||||
private static Map<String, Object> apiRequest(String endpoint, String method, Map<String, Object> data, String publicKey, String secretKey) throws Exception {
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
String body = data != null ? toJson(data) : "";
|
||||
String signatureData = timestamp + ":" + method + ":" + endpoint + ":" + body;
|
||||
String signature = hmacSha256(secretKey, signatureData);
|
||||
|
||||
URL url = new URL(API_BASE + endpoint);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod(method);
|
||||
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||
conn.setRequestProperty("Authorization", "Bearer " + (publicKey != null ? publicKey : secretKey));
|
||||
conn.setRequestProperty("X-Timestamp", String.valueOf(timestamp));
|
||||
conn.setRequestProperty("X-Signature", signature);
|
||||
conn.setRequestProperty("Content-Type", "application/json");
|
||||
conn.setConnectTimeout(30000);
|
||||
conn.setReadTimeout(300000);
|
||||
|
||||
if (data != null) {
|
||||
conn.setDoOutput(true);
|
||||
String json = toJson(data);
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(json.getBytes("UTF-8"));
|
||||
os.write(body.getBytes("UTF-8"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,19 @@ GREEN='\033[0;32m'
|
|||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if UNSANDBOX_API_KEY is set
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo -e "${RED}Error: UNSANDBOX_API_KEY environment variable not set${NC}"
|
||||
echo "Please set it with: export UNSANDBOX_API_KEY=usk_your_key_here"
|
||||
exit 1
|
||||
# Check if UNSANDBOX auth keys are set (HMAC or legacy)
|
||||
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
|
||||
# Fall back to legacy API key
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo -e "${RED}Error: UNSANDBOX authentication not configured${NC}"
|
||||
echo "Please set HMAC auth keys:"
|
||||
echo " export UNSANDBOX_PUBLIC_KEY=usk_pub_your_key_here"
|
||||
echo " export UNSANDBOX_SECRET_KEY=usk_sec_your_key_here"
|
||||
echo ""
|
||||
echo "Or use legacy authentication:"
|
||||
echo " export UNSANDBOX_API_KEY=usk_your_key_here"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if un binary exists
|
||||
|
|
|
|||
|
|
@ -22,12 +22,16 @@ echo -e "${CYAN}║ 42 Languages × 3 Test Types = The Matrix
|
|||
echo -e "${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
# Check API key
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo -e "${YELLOW}WARNING:${NC} UNSANDBOX_API_KEY not set"
|
||||
echo "Integration and functional tests will be skipped"
|
||||
echo "Run: source ../../vars.sh"
|
||||
echo ""
|
||||
# Check API auth keys
|
||||
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo -e "${YELLOW}WARNING:${NC} UNSANDBOX authentication not configured"
|
||||
echo "Integration and functional tests will be skipped"
|
||||
echo "Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
|
||||
echo "Or legacy key: export UNSANDBOX_API_KEY=..."
|
||||
echo "Run: source ../../vars.sh"
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Counters
|
||||
|
|
|
|||
|
|
@ -40,11 +40,15 @@ run_test() {
|
|||
echo ""
|
||||
}
|
||||
|
||||
# Check for API key
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo "⚠ WARNING: UNSANDBOX_API_KEY not set"
|
||||
echo " Integration and functional tests will be skipped"
|
||||
echo ""
|
||||
# Check for API auth keys
|
||||
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo "⚠ WARNING: UNSANDBOX authentication not configured"
|
||||
echo " Integration and functional tests will be skipped"
|
||||
echo " Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
|
||||
echo " Or legacy key: export UNSANDBOX_API_KEY=..."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run tests for each language
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ echo "UN CLI Inception Compiled Languages Test Runner"
|
|||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check for API key
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo "WARNING: UNSANDBOX_API_KEY not set"
|
||||
echo "API and functional tests will be skipped"
|
||||
echo ""
|
||||
# Check for API auth keys
|
||||
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo "WARNING: UNSANDBOX authentication not configured"
|
||||
echo "API and functional tests will be skipped"
|
||||
echo "Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
|
||||
echo "Or legacy key: export UNSANDBOX_API_KEY=..."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
|
|
|||
|
|
@ -25,10 +25,14 @@ echo -e "${CYAN}╚════════════════════
|
|||
echo ""
|
||||
|
||||
# Check requirements
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo -e "${RED}ERROR:${NC} UNSANDBOX_API_KEY not set"
|
||||
echo "Run: source ../../vars.sh"
|
||||
exit 1
|
||||
if [ -z "$UNSANDBOX_PUBLIC_KEY" ] || [ -z "$UNSANDBOX_SECRET_KEY" ]; then
|
||||
if [ -z "$UNSANDBOX_API_KEY" ]; then
|
||||
echo -e "${RED}ERROR:${NC} UNSANDBOX authentication not configured"
|
||||
echo "Set HMAC keys: export UNSANDBOX_PUBLIC_KEY=... UNSANDBOX_SECRET_KEY=..."
|
||||
echo "Or legacy key: export UNSANDBOX_API_KEY=..."
|
||||
echo "Run: source ../../vars.sh"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$CLI_DIR/un2" ]; then
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ print_test() {
|
|||
|
||||
if [ "$result" = "true" ]; then
|
||||
echo -e "${GREEN}✓ PASS${RESET}: $name"
|
||||
((PASSED++))
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo -e "${RED}✗ FAIL${RESET}: $name"
|
||||
((FAILED++))
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,25 +133,40 @@ async function runTests() {
|
|||
results.failTest('Extension detection: .unknown -> undefined', e.message);
|
||||
}
|
||||
|
||||
// Test 7: API call test (requires UNSANDBOX_API_KEY)
|
||||
if (!process.env.UNSANDBOX_API_KEY) {
|
||||
results.skipTest('API call test', 'UNSANDBOX_API_KEY not set');
|
||||
// Test 7: API call test (requires UNSANDBOX auth)
|
||||
const hasHMAC = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
|
||||
const hasLegacy = process.env.UNSANDBOX_API_KEY;
|
||||
if (!hasHMAC && !hasLegacy) {
|
||||
results.skipTest('API call test', 'UNSANDBOX authentication not configured');
|
||||
} else {
|
||||
try {
|
||||
const https = require('https');
|
||||
const apiKey = process.env.UNSANDBOX_API_KEY;
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Use HMAC auth if available, otherwise fall back to legacy
|
||||
const publicKey = process.env.UNSANDBOX_PUBLIC_KEY || process.env.UNSANDBOX_API_KEY;
|
||||
const secretKey = process.env.UNSANDBOX_SECRET_KEY || process.env.UNSANDBOX_API_KEY;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
language: 'python',
|
||||
code: 'print("Hello from API")'
|
||||
});
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const signatureInput = `${timestamp}:POST:/execute:${payload}`;
|
||||
const signature = crypto.createHmac('sha256', secretKey)
|
||||
.update(signatureInput)
|
||||
.digest('hex');
|
||||
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'api.unsandbox.com',
|
||||
path: '/execute',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Authorization': `Bearer ${publicKey}`,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Signature': signature,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload)
|
||||
}
|
||||
|
|
@ -185,8 +200,10 @@ async function runTests() {
|
|||
}
|
||||
|
||||
// Test 8: End-to-end test with fib.py
|
||||
if (!process.env.UNSANDBOX_API_KEY) {
|
||||
results.skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set');
|
||||
const hasHMAC2 = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
|
||||
const hasLegacy2 = process.env.UNSANDBOX_API_KEY;
|
||||
if (!hasHMAC2 && !hasLegacy2) {
|
||||
results.skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not configured');
|
||||
} else if (!fs.existsSync(FIB_PY)) {
|
||||
results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ local has_ltn12, ltn12 = pcall(require, "ltn12")
|
|||
local has_json, json = pcall(require, "cjson")
|
||||
|
||||
-- Test configuration
|
||||
local script_dir = arg[0]:match("(.*/)")
|
||||
local script_dir = arg[0]:match("(.*/)") or "./"
|
||||
local UN_SCRIPT = script_dir .. "../un.lua"
|
||||
local FIB_PY = script_dir .. "../../test/fib.py"
|
||||
|
||||
|
|
@ -144,9 +144,11 @@ if not status then
|
|||
results:failTest('Extension detection: .unknown -> nil', err)
|
||||
end
|
||||
|
||||
-- Test 7: API call test (requires UNSANDBOX_API_KEY)
|
||||
if not os.getenv("UNSANDBOX_API_KEY") then
|
||||
results:skipTest('API call test', 'UNSANDBOX_API_KEY not set')
|
||||
-- Test 7: API call test (requires UNSANDBOX auth)
|
||||
local has_hmac = os.getenv("UNSANDBOX_PUBLIC_KEY") and os.getenv("UNSANDBOX_SECRET_KEY")
|
||||
local has_legacy = os.getenv("UNSANDBOX_API_KEY")
|
||||
if not (has_hmac or has_legacy) then
|
||||
results:skipTest('API call test', 'UNSANDBOX authentication not set')
|
||||
elseif not (has_https and has_ltn12 and has_json) then
|
||||
results:skipTest('API call test', 'Required Lua libraries not available (luasocket, luasec, lua-cjson)')
|
||||
else
|
||||
|
|
@ -186,8 +188,8 @@ else
|
|||
end
|
||||
|
||||
-- Test 8: End-to-end test with fib.py
|
||||
if not os.getenv("UNSANDBOX_API_KEY") then
|
||||
results:skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set')
|
||||
if not (has_hmac or has_legacy) then
|
||||
results:skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not set')
|
||||
else
|
||||
-- Check if fib.py exists
|
||||
local file = io.open(FIB_PY, "r")
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ except Exception as e:
|
|||
|
||||
# Test 6: Extension detection for unknown extension
|
||||
try:
|
||||
lang = un.detect_language('test.unknown')
|
||||
lang = un.detect_language('test.unknown', exit_on_error=False)
|
||||
if lang is None:
|
||||
results.pass_test("Extension detection: .unknown -> None")
|
||||
else:
|
||||
|
|
@ -99,9 +99,11 @@ try:
|
|||
except Exception as e:
|
||||
results.fail_test("Extension detection: .unknown -> None", str(e))
|
||||
|
||||
# Test 7: API call test (requires UNSANDBOX_API_KEY)
|
||||
if not os.environ.get('UNSANDBOX_API_KEY'):
|
||||
results.skip_test("API call test", "UNSANDBOX_API_KEY not set")
|
||||
# Test 7: API call test (requires UNSANDBOX auth)
|
||||
has_hmac = os.environ.get('UNSANDBOX_PUBLIC_KEY') and os.environ.get('UNSANDBOX_SECRET_KEY')
|
||||
has_legacy = os.environ.get('UNSANDBOX_API_KEY')
|
||||
if not (has_hmac or has_legacy):
|
||||
results.skip_test("API call test", "UNSANDBOX authentication not configured")
|
||||
else:
|
||||
try:
|
||||
result = un.execute_code('python', 'print("Hello from API")')
|
||||
|
|
@ -113,8 +115,10 @@ else:
|
|||
results.fail_test("API call test", str(e))
|
||||
|
||||
# Test 8: End-to-end test with fib.py
|
||||
if not os.environ.get('UNSANDBOX_API_KEY'):
|
||||
results.skip_test("End-to-end fib.py test", "UNSANDBOX_API_KEY not set")
|
||||
has_hmac = os.environ.get('UNSANDBOX_PUBLIC_KEY') and os.environ.get('UNSANDBOX_SECRET_KEY')
|
||||
has_legacy = os.environ.get('UNSANDBOX_API_KEY')
|
||||
if not (has_hmac or has_legacy):
|
||||
results.skip_test("End-to-end fib.py test", "UNSANDBOX authentication not configured")
|
||||
elif not os.path.exists(FIB_PY):
|
||||
results.skip_test("End-to-end fib.py test", f"fib.py not found at {FIB_PY}")
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -21,14 +21,14 @@ TESTS_FAILED=0
|
|||
|
||||
# Test result tracking
|
||||
test_passed() {
|
||||
((TESTS_PASSED++))
|
||||
((TESTS_RUN++))
|
||||
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
echo -e "${GREEN}✓ PASS${NC}: $1"
|
||||
}
|
||||
|
||||
test_failed() {
|
||||
((TESTS_FAILED++))
|
||||
((TESTS_RUN++))
|
||||
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||
TESTS_RUN=$((TESTS_RUN + 1))
|
||||
echo -e "${RED}✗ FAIL${NC}: $1"
|
||||
if [ -n "${2:-}" ]; then
|
||||
echo -e "${RED} Error: $2${NC}"
|
||||
|
|
@ -69,10 +69,10 @@ fi
|
|||
if output=$("$UN_SH" /tmp/nonexistent_file_12345.xyz 2>&1); then
|
||||
test_failed "Handles non-existent file" "Should exit with error"
|
||||
else
|
||||
if echo "$output" | grep -q "not found"; then
|
||||
if echo "$output" | grep -qi "not found\|error"; then
|
||||
test_passed "Handles non-existent file"
|
||||
else
|
||||
test_failed "Handles non-existent file" "Expected 'not found' message"
|
||||
test_failed "Handles non-existent file" "Expected error message, got: $output"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
|
@ -83,40 +83,50 @@ if output=$("$UN_SH" "$UNKNOWN_FILE" 2>&1); then
|
|||
test_failed "Handles unknown file extension" "Should exit with error"
|
||||
rm -f "$UNKNOWN_FILE"
|
||||
else
|
||||
if echo "$output" | grep -q "Unknown file extension"; then
|
||||
if echo "$output" | grep -qi "cannot detect\|unknown\|error"; then
|
||||
test_passed "Handles unknown file extension"
|
||||
else
|
||||
test_failed "Handles unknown file extension" "Expected 'Unknown file extension' message"
|
||||
test_failed "Handles unknown file extension" "Expected error message, got: $output"
|
||||
fi
|
||||
rm -f "$UNKNOWN_FILE"
|
||||
fi
|
||||
|
||||
# Test: Error when API key not set
|
||||
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
|
||||
# Test: Error when auth not set
|
||||
has_hmac="${UNSANDBOX_PUBLIC_KEY:-}${UNSANDBOX_SECRET_KEY:-}"
|
||||
has_legacy="${UNSANDBOX_API_KEY:-}"
|
||||
if [ -n "$has_hmac" ] || [ -n "$has_legacy" ]; then
|
||||
TEST_FILE="$TEST_DIR/fib.py"
|
||||
if [ -f "$TEST_FILE" ]; then
|
||||
# Temporarily unset API key
|
||||
OLD_KEY="$UNSANDBOX_API_KEY"
|
||||
# Temporarily unset auth keys
|
||||
OLD_PUB="${UNSANDBOX_PUBLIC_KEY:-}"
|
||||
OLD_SEC="${UNSANDBOX_SECRET_KEY:-}"
|
||||
OLD_KEY="${UNSANDBOX_API_KEY:-}"
|
||||
unset UNSANDBOX_PUBLIC_KEY
|
||||
unset UNSANDBOX_SECRET_KEY
|
||||
unset UNSANDBOX_API_KEY
|
||||
if output=$("$UN_SH" "$TEST_FILE" 2>&1); then
|
||||
test_failed "Requires API key" "Should exit with error when API key not set"
|
||||
test_failed "Requires authentication" "Should exit with error when auth not set"
|
||||
else
|
||||
if echo "$output" | grep -q "UNSANDBOX_API_KEY"; then
|
||||
test_passed "Requires API key"
|
||||
if echo "$output" | grep -qE "UNSANDBOX_(API_KEY|PUBLIC_KEY|SECRET_KEY)"; then
|
||||
test_passed "Requires authentication"
|
||||
else
|
||||
test_failed "Requires API key" "Expected API key error message"
|
||||
test_failed "Requires authentication" "Expected auth error message"
|
||||
fi
|
||||
fi
|
||||
export UNSANDBOX_API_KEY="$OLD_KEY"
|
||||
[ -n "$OLD_PUB" ] && export UNSANDBOX_PUBLIC_KEY="$OLD_PUB"
|
||||
[ -n "$OLD_SEC" ] && export UNSANDBOX_SECRET_KEY="$OLD_SEC"
|
||||
[ -n "$OLD_KEY" ] && export UNSANDBOX_API_KEY="$OLD_KEY"
|
||||
else
|
||||
test_skipped "Requires API key (test file not found)"
|
||||
test_skipped "Requires authentication (test file not found)"
|
||||
fi
|
||||
else
|
||||
test_skipped "Requires API key (API key already not set)"
|
||||
test_skipped "Requires authentication (auth already not set)"
|
||||
fi
|
||||
|
||||
# Integration Tests (require API key)
|
||||
if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
|
||||
# Integration Tests (require auth)
|
||||
has_hmac="${UNSANDBOX_PUBLIC_KEY:-}${UNSANDBOX_SECRET_KEY:-}"
|
||||
has_legacy="${UNSANDBOX_API_KEY:-}"
|
||||
if [ -n "$has_hmac" ] || [ -n "$has_legacy" ]; then
|
||||
echo -e "\n${BLUE}=== Integration Tests for un.sh ===${NC}"
|
||||
|
||||
# Test: Can execute Python file
|
||||
|
|
@ -149,7 +159,7 @@ if [ -n "${UNSANDBOX_API_KEY:-}" ]; then
|
|||
test_skipped "Executes Bash file successfully (fib.sh not found)"
|
||||
fi
|
||||
else
|
||||
echo -e "\n${YELLOW}Skipping integration tests (UNSANDBOX_API_KEY not set)${NC}"
|
||||
echo -e "\n${YELLOW}Skipping integration tests (UNSANDBOX authentication not configured)${NC}"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ proc run_command {cmd} {
|
|||
}
|
||||
}
|
||||
|
||||
# Check if required TCL packages are available
|
||||
set has_required_packages 1
|
||||
foreach pkg {http json tls base64 sha256} {
|
||||
if {[catch {package require $pkg}]} {
|
||||
set has_required_packages 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Unit Tests
|
||||
puts "[color_blue "=== Unit Tests for un.tcl ==="]"
|
||||
|
||||
|
|
@ -57,74 +66,102 @@ if {[file exists $UN_TCL] && [file executable $UN_TCL]} {
|
|||
test_failed "Script exists and is executable" "File not found or not executable"
|
||||
}
|
||||
|
||||
# Test: Usage message when no arguments
|
||||
set result [run_command [list $UN_TCL]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
|
||||
if {$exit_code != 0 && [string match "*Usage:*" $output]} {
|
||||
test_passed "Shows usage message with no arguments"
|
||||
if {!$has_required_packages} {
|
||||
test_skipped "Shows usage message with no arguments (missing TCL packages)"
|
||||
test_skipped "Handles non-existent file (missing TCL packages)"
|
||||
test_skipped "Handles unknown file extension (missing TCL packages)"
|
||||
} else {
|
||||
test_failed "Shows usage message with no arguments" "Expected usage message"
|
||||
# Test: Usage message when no arguments
|
||||
set result [run_command [list $UN_TCL]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
|
||||
if {$exit_code != 0 && [string match "*Usage:*" $output]} {
|
||||
test_passed "Shows usage message with no arguments"
|
||||
} else {
|
||||
test_failed "Shows usage message with no arguments" "Expected usage message"
|
||||
}
|
||||
|
||||
# Test: Error on non-existent file
|
||||
set result [run_command [list $UN_TCL /tmp/nonexistent_file_12345.xyz]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
|
||||
if {$exit_code != 0 && ([string match "*not found*" $output] || [string match "*does not exist*" $output] || [string match "*Error:*" $output])} {
|
||||
test_passed "Handles non-existent file"
|
||||
} else {
|
||||
test_failed "Handles non-existent file" "Expected error message"
|
||||
}
|
||||
|
||||
# Test: Error on unknown extension
|
||||
set unknown_file "/tmp/test_unknown_ext_[pid].unknownext"
|
||||
set fp [open $unknown_file w]
|
||||
puts $fp "test"
|
||||
close $fp
|
||||
|
||||
set result [run_command [list $UN_TCL $unknown_file]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
|
||||
file delete $unknown_file
|
||||
|
||||
if {$exit_code != 0 && ([string match "*unknown*" $output] || [string match "*extension*" $output] || [string match "*Error:*" $output] || [string match "*cannot detect*" $output])} {
|
||||
test_passed "Handles unknown file extension"
|
||||
} else {
|
||||
test_failed "Handles unknown file extension" "Expected extension error message"
|
||||
}
|
||||
}
|
||||
|
||||
# Test: Error on non-existent file
|
||||
set result [run_command [list $UN_TCL /tmp/nonexistent_file_12345.xyz]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
# Test: Error when authentication not set
|
||||
set has_hmac [expr {[info exists ::env(UNSANDBOX_PUBLIC_KEY)] && $::env(UNSANDBOX_PUBLIC_KEY) ne "" && [info exists ::env(UNSANDBOX_SECRET_KEY)] && $::env(UNSANDBOX_SECRET_KEY) ne ""}]
|
||||
set has_legacy [expr {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""}]
|
||||
|
||||
if {$exit_code != 0 && [string match "*not found*" $output]} {
|
||||
test_passed "Handles non-existent file"
|
||||
} else {
|
||||
test_failed "Handles non-existent file" "Expected 'not found' message"
|
||||
}
|
||||
|
||||
# Test: Error on unknown extension
|
||||
set unknown_file "/tmp/test_unknown_ext_[pid].unknownext"
|
||||
set fp [open $unknown_file w]
|
||||
puts $fp "test"
|
||||
close $fp
|
||||
|
||||
set result [run_command [list $UN_TCL $unknown_file]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
|
||||
file delete $unknown_file
|
||||
|
||||
if {$exit_code != 0 && [string match "*Unknown file extension*" $output]} {
|
||||
test_passed "Handles unknown file extension"
|
||||
} else {
|
||||
test_failed "Handles unknown file extension" "Expected 'Unknown file extension' message"
|
||||
}
|
||||
|
||||
# Test: Error when API key not set
|
||||
if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} {
|
||||
if {!$has_required_packages} {
|
||||
test_skipped "Requires authentication (missing TCL packages)"
|
||||
} elseif {$has_hmac || $has_legacy} {
|
||||
set test_file [file join $TEST_DIR fib.py]
|
||||
if {[file exists $test_file]} {
|
||||
# Temporarily unset API key
|
||||
set old_key $::env(UNSANDBOX_API_KEY)
|
||||
unset ::env(UNSANDBOX_API_KEY)
|
||||
# Temporarily unset auth keys
|
||||
if {$has_hmac} {
|
||||
set old_pub $::env(UNSANDBOX_PUBLIC_KEY)
|
||||
set old_sec $::env(UNSANDBOX_SECRET_KEY)
|
||||
unset ::env(UNSANDBOX_PUBLIC_KEY)
|
||||
unset ::env(UNSANDBOX_SECRET_KEY)
|
||||
}
|
||||
if {$has_legacy} {
|
||||
set old_key $::env(UNSANDBOX_API_KEY)
|
||||
unset ::env(UNSANDBOX_API_KEY)
|
||||
}
|
||||
|
||||
set result [run_command [list $UN_TCL $test_file]]
|
||||
set exit_code [lindex $result 0]
|
||||
set output [lindex $result 1]
|
||||
|
||||
set ::env(UNSANDBOX_API_KEY) $old_key
|
||||
# Restore keys
|
||||
if {$has_hmac} {
|
||||
set ::env(UNSANDBOX_PUBLIC_KEY) $old_pub
|
||||
set ::env(UNSANDBOX_SECRET_KEY) $old_sec
|
||||
}
|
||||
if {$has_legacy} {
|
||||
set ::env(UNSANDBOX_API_KEY) $old_key
|
||||
}
|
||||
|
||||
if {$exit_code != 0 && [string match "*UNSANDBOX_API_KEY*" $output]} {
|
||||
test_passed "Requires API key"
|
||||
if {$exit_code != 0 && ([string match "*UNSANDBOX_PUBLIC_KEY*" $output] || [string match "*UNSANDBOX_API_KEY*" $output])} {
|
||||
test_passed "Requires authentication"
|
||||
} else {
|
||||
test_failed "Requires API key" "Expected API key error message"
|
||||
test_failed "Requires authentication" "Expected auth error message"
|
||||
}
|
||||
} else {
|
||||
test_skipped "Requires API key (test file not found)"
|
||||
test_skipped "Requires authentication (test file not found)"
|
||||
}
|
||||
} else {
|
||||
test_skipped "Requires API key (API key already not set)"
|
||||
test_skipped "Requires authentication (auth already not set)"
|
||||
}
|
||||
|
||||
# Integration Tests (require API key)
|
||||
if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} {
|
||||
# Integration Tests (require authentication and packages)
|
||||
if {!$has_required_packages} {
|
||||
puts "\n[color_yellow "Skipping integration tests (missing TCL packages)"]"
|
||||
} elseif {$has_hmac || $has_legacy} {
|
||||
puts "\n[color_blue "=== Integration Tests for un.tcl ==="]"
|
||||
|
||||
# Test: Can execute Python file
|
||||
|
|
@ -159,7 +196,7 @@ if {[info exists ::env(UNSANDBOX_API_KEY)] && $::env(UNSANDBOX_API_KEY) ne ""} {
|
|||
test_skipped "Executes Bash file successfully (fib.sh not found)"
|
||||
}
|
||||
} else {
|
||||
puts "\n[color_yellow "Skipping integration tests (UNSANDBOX_API_KEY not set)"]"
|
||||
puts "\n[color_yellow "Skipping integration tests (UNSANDBOX authentication not configured)"]"
|
||||
}
|
||||
|
||||
# Summary
|
||||
|
|
|
|||
|
|
@ -11,12 +11,16 @@ import * as path from 'path';
|
|||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import * as https from 'https';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// Get script directory - works in both CommonJS and ES modules
|
||||
const SCRIPT_DIR = path.dirname(process.argv[1] || __filename);
|
||||
|
||||
// Test configuration
|
||||
const UN_SCRIPT = path.join(__dirname, '..', 'un.ts');
|
||||
const FIB_PY = path.join(__dirname, '..', '..', 'test', 'fib.py');
|
||||
const UN_SCRIPT = path.join(SCRIPT_DIR, '..', 'un.ts');
|
||||
const FIB_PY = path.join(SCRIPT_DIR, '..', '..', 'test', 'fib.py');
|
||||
|
||||
class TestResults {
|
||||
passed: number = 0;
|
||||
|
|
@ -140,26 +144,37 @@ async function runTests(): Promise<void> {
|
|||
results.failTest('Extension detection: .unknown -> undefined', (e as Error).message);
|
||||
}
|
||||
|
||||
// Test 7: API call test (requires UNSANDBOX_API_KEY)
|
||||
if (!process.env.UNSANDBOX_API_KEY) {
|
||||
results.skipTest('API call test', 'UNSANDBOX_API_KEY not set');
|
||||
// Test 7: API call test (requires UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY)
|
||||
const hasHmac = process.env.UNSANDBOX_PUBLIC_KEY && process.env.UNSANDBOX_SECRET_KEY;
|
||||
const hasLegacy = process.env.UNSANDBOX_API_KEY;
|
||||
if (!hasHmac && !hasLegacy) {
|
||||
results.skipTest('API call test', 'UNSANDBOX authentication not set');
|
||||
} else {
|
||||
try {
|
||||
const apiKey = process.env.UNSANDBOX_API_KEY;
|
||||
const publicKey = process.env.UNSANDBOX_PUBLIC_KEY || process.env.UNSANDBOX_API_KEY || '';
|
||||
const secretKey = process.env.UNSANDBOX_SECRET_KEY || '';
|
||||
const payload = JSON.stringify({
|
||||
language: 'python',
|
||||
code: 'print("Hello from API")'
|
||||
});
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const method = 'POST';
|
||||
const apiPath = '/execute';
|
||||
const signatureData = `${timestamp}:${method}:${apiPath}:${payload}`;
|
||||
const signature = crypto.createHmac('sha256', secretKey).update(signatureData).digest('hex');
|
||||
|
||||
const result: ExecuteResult = await new Promise((resolve, reject) => {
|
||||
const options: https.RequestOptions = {
|
||||
hostname: 'api.unsandbox.com',
|
||||
path: '/execute',
|
||||
method: 'POST',
|
||||
path: apiPath,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Authorization': `Bearer ${publicKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload)
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Signature': signature
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -191,8 +206,8 @@ async function runTests(): Promise<void> {
|
|||
}
|
||||
|
||||
// Test 8: End-to-end test with fib.py
|
||||
if (!process.env.UNSANDBOX_API_KEY) {
|
||||
results.skipTest('End-to-end fib.py test', 'UNSANDBOX_API_KEY not set');
|
||||
if (!hasHmac && !hasLegacy) {
|
||||
results.skipTest('End-to-end fib.py test', 'UNSANDBOX authentication not set');
|
||||
} else if (!fs.existsSync(FIB_PY)) {
|
||||
results.skipTest('End-to-end fib.py test', `fib.py not found at ${FIB_PY}`);
|
||||
} else {
|
||||
|
|
|
|||
174
un.awk
174
un.awk
|
|
@ -60,15 +60,32 @@ BEGIN {
|
|||
RESET = "\033[0m"
|
||||
}
|
||||
|
||||
function get_api_key() {
|
||||
cmd = "echo $UNSANDBOX_API_KEY"
|
||||
cmd | getline api_key
|
||||
function get_api_keys( public_key, secret_key, cmd) {
|
||||
# Get public key
|
||||
cmd = "echo -n $UNSANDBOX_PUBLIC_KEY"
|
||||
cmd | getline public_key
|
||||
close(cmd)
|
||||
if (api_key == "") {
|
||||
print RED "Error: UNSANDBOX_API_KEY not set" RESET > "/dev/stderr"
|
||||
|
||||
# Get secret key
|
||||
cmd = "echo -n $UNSANDBOX_SECRET_KEY"
|
||||
cmd | getline secret_key
|
||||
close(cmd)
|
||||
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if (public_key == "") {
|
||||
cmd = "echo -n $UNSANDBOX_API_KEY"
|
||||
cmd | getline public_key
|
||||
close(cmd)
|
||||
secret_key = ""
|
||||
}
|
||||
|
||||
if (public_key == "") {
|
||||
print RED "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" RESET > "/dev/stderr"
|
||||
exit 1
|
||||
}
|
||||
return api_key
|
||||
|
||||
GLOBAL_PUBLIC_KEY = public_key
|
||||
GLOBAL_SECRET_KEY = secret_key
|
||||
}
|
||||
|
||||
function get_extension(filename) {
|
||||
|
|
@ -88,8 +105,9 @@ function escape_json(s) {
|
|||
return s
|
||||
}
|
||||
|
||||
function execute(filename) {
|
||||
api_key = get_api_key()
|
||||
function execute(filename , api_key) {
|
||||
get_api_keys()
|
||||
api_key = GLOBAL_PUBLIC_KEY
|
||||
|
||||
# Get extension and language
|
||||
ext = get_extension(filename)
|
||||
|
|
@ -119,10 +137,27 @@ function execute(filename) {
|
|||
print json > tmp
|
||||
close(tmp)
|
||||
|
||||
# Build HMAC signature if secret key exists
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
# HMAC signature: timestamp:METHOD:path:body
|
||||
sig_input = timestamp ":POST:/execute:" json
|
||||
sig_tmp = "/tmp/un_awk_sig_" PROCINFO["pid"]
|
||||
print sig_input > sig_tmp
|
||||
close(sig_tmp)
|
||||
sig_cmd = "echo -n '" sig_input "' | openssl dgst -sha256 -hmac '" GLOBAL_SECRET_KEY "' | sed 's/^.* //'"
|
||||
sig_cmd | getline signature
|
||||
close(sig_cmd)
|
||||
system("rm -f " sig_tmp)
|
||||
sig_headers = "-H 'X-Timestamp: " timestamp "' -H 'X-Signature: " signature "' "
|
||||
}
|
||||
|
||||
# Call curl
|
||||
cmd = "curl -s -X POST '" API_BASE "/execute' " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-H 'Authorization: Bearer " api_key "' " \
|
||||
sig_headers \
|
||||
"-d '@" tmp "'"
|
||||
|
||||
response = ""
|
||||
|
|
@ -160,43 +195,94 @@ function execute(filename) {
|
|||
}
|
||||
}
|
||||
|
||||
function session_list() {
|
||||
api_key = get_api_key()
|
||||
cmd = "curl -s '" API_BASE "/sessions' -H 'Authorization: Bearer " api_key "'"
|
||||
function session_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:/sessions:"
|
||||
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 "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE "/sessions' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function session_kill(id) {
|
||||
api_key = get_api_key()
|
||||
cmd = "curl -s -X DELETE '" API_BASE "/sessions/" id "' -H 'Authorization: Bearer " api_key "'"
|
||||
function session_kill(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/sessions/" id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":DELETE:" endpoint ":"
|
||||
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 "'"
|
||||
}
|
||||
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
system(cmd)
|
||||
print GREEN "Session terminated: " id RESET
|
||||
}
|
||||
|
||||
function service_list() {
|
||||
api_key = get_api_key()
|
||||
cmd = "curl -s '" API_BASE "/services' -H 'Authorization: Bearer " api_key "'"
|
||||
function service_list( timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":GET:/services:"
|
||||
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 "'"
|
||||
}
|
||||
cmd = "curl -s '" API_BASE "/services' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
while ((cmd | getline line) > 0) print line
|
||||
close(cmd)
|
||||
}
|
||||
|
||||
function service_destroy(id) {
|
||||
api_key = get_api_key()
|
||||
cmd = "curl -s -X DELETE '" API_BASE "/services/" id "' -H 'Authorization: Bearer " api_key "'"
|
||||
function service_destroy(id , timestamp, sig_headers, signature, sig_input, sig_cmd, endpoint) {
|
||||
get_api_keys()
|
||||
endpoint = "/services/" id
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":DELETE:" endpoint ":"
|
||||
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 "'"
|
||||
}
|
||||
cmd = "curl -s -X DELETE '" API_BASE endpoint "' -H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " sig_headers
|
||||
system(cmd)
|
||||
print GREEN "Service destroyed: " id RESET
|
||||
}
|
||||
|
||||
function service_dump_bootstrap(id, dump_file) {
|
||||
api_key = get_api_key()
|
||||
function service_dump_bootstrap(id, dump_file , endpoint, json_body, timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
print "Fetching bootstrap script from " id "..." > "/dev/stderr"
|
||||
|
||||
endpoint = "/services/" id "/execute"
|
||||
json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}"
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:" endpoint ":" json_body
|
||||
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 "' "
|
||||
}
|
||||
|
||||
# Build the curl command to execute on the service
|
||||
cmd = "curl -s -X POST '" API_BASE "/services/" id "/execute' " \
|
||||
cmd = "curl -s -X POST '" API_BASE endpoint "' " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-H 'Authorization: Bearer " api_key "' " \
|
||||
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"
|
||||
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||
sig_headers \
|
||||
"-d '" json_body "'"
|
||||
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
|
|
@ -229,8 +315,8 @@ function service_dump_bootstrap(id, dump_file) {
|
|||
}
|
||||
}
|
||||
|
||||
function service_create(name, ports, domains, service_type, bootstrap) {
|
||||
api_key = get_api_key()
|
||||
function service_create(name, ports, domains, service_type, bootstrap , json, tmp, timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
|
||||
# Build JSON payload
|
||||
json = "{\"name\":\"" escape_json(name) "\""
|
||||
|
|
@ -265,10 +351,22 @@ function service_create(name, ports, domains, service_type, bootstrap) {
|
|||
print json > tmp
|
||||
close(tmp)
|
||||
|
||||
# Build HMAC signature if secret key exists
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:/services:" 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 "/services' " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-H 'Authorization: Bearer " api_key "' " \
|
||||
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||
sig_headers \
|
||||
"-d '@" tmp "'"
|
||||
|
||||
response = ""
|
||||
|
|
@ -284,11 +382,24 @@ function service_create(name, ports, domains, service_type, bootstrap) {
|
|||
print response
|
||||
}
|
||||
|
||||
function validate_key(api_key, do_extend) {
|
||||
function validate_key(do_extend , timestamp, sig_headers, signature, sig_input, sig_cmd) {
|
||||
get_api_keys()
|
||||
|
||||
timestamp = systime()
|
||||
sig_headers = ""
|
||||
if (GLOBAL_SECRET_KEY != "") {
|
||||
sig_input = timestamp ":POST:/keys/validate:"
|
||||
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 to validate key
|
||||
cmd = "curl -s -X POST '" PORTAL_BASE "/keys/validate' " \
|
||||
"-H 'Content-Type: application/json' " \
|
||||
"-H 'Authorization: Bearer " api_key "'"
|
||||
"-H 'Authorization: Bearer " GLOBAL_PUBLIC_KEY "' " \
|
||||
sig_headers
|
||||
|
||||
response = ""
|
||||
while ((cmd | getline line) > 0) {
|
||||
|
|
@ -366,8 +477,7 @@ function validate_key(api_key, do_extend) {
|
|||
}
|
||||
|
||||
function cmd_key(do_extend) {
|
||||
api_key = get_api_key()
|
||||
validate_key(api_key, do_extend)
|
||||
validate_key(do_extend)
|
||||
}
|
||||
|
||||
function show_help() {
|
||||
|
|
|
|||
85
un.clj
85
un.clj
|
|
@ -101,41 +101,82 @@
|
|||
(or (second (re-find pattern-str json-str))
|
||||
(second (re-find pattern-num json-str)))))
|
||||
|
||||
(defn get-api-keys []
|
||||
(let [public-key (System/getenv "UNSANDBOX_PUBLIC_KEY")
|
||||
secret-key (System/getenv "UNSANDBOX_SECRET_KEY")
|
||||
api-key (System/getenv "UNSANDBOX_API_KEY")]
|
||||
(cond
|
||||
(and public-key secret-key) [public-key secret-key]
|
||||
api-key [api-key nil]
|
||||
:else (do
|
||||
(binding [*out* *err*]
|
||||
(println "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn get-api-key []
|
||||
(or (System/getenv "UNSANDBOX_API_KEY")
|
||||
(do (binding [*out* *err*]
|
||||
(println "Error: UNSANDBOX_API_KEY not set"))
|
||||
(System/exit 1))))
|
||||
(first (get-api-keys)))
|
||||
|
||||
(defn hmac-sha256 [secret message]
|
||||
(let [mac (javax.crypto.Mac/getInstance "HmacSHA256")
|
||||
secret-key (javax.crypto.spec.SecretKeySpec. (.getBytes secret "UTF-8") "HmacSHA256")]
|
||||
(.init mac secret-key)
|
||||
(let [bytes (.doFinal mac (.getBytes message "UTF-8"))]
|
||||
(apply str (map #(format "%02x" %) bytes)))))
|
||||
|
||||
(defn make-signature [secret-key timestamp method path body]
|
||||
(let [message (str timestamp ":" method ":" path ":" body)]
|
||||
(hmac-sha256 secret-key message)))
|
||||
|
||||
(defn build-auth-headers [public-key secret-key method path body]
|
||||
(if secret-key
|
||||
(let [timestamp (str (quot (System/currentTimeMillis) 1000))
|
||||
signature (make-signature secret-key timestamp method path body)]
|
||||
["-H" (str "Authorization: Bearer " public-key)
|
||||
"-H" (str "X-Timestamp: " timestamp)
|
||||
"-H" (str "X-Signature: " signature)])
|
||||
["-H" (str "Authorization: Bearer " public-key)]))
|
||||
|
||||
(defn curl-post [api-key endpoint json-data]
|
||||
(let [tmp-file (str "/tmp/un_clj_" (rand-int 999999) ".json")]
|
||||
(let [tmp-file (str "/tmp/un_clj_" (rand-int 999999) ".json")
|
||||
[public-key secret-key] (get-api-keys)
|
||||
auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)]
|
||||
(spit tmp-file json-data)
|
||||
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST"
|
||||
(str "https://api.unsandbox.com" endpoint)
|
||||
"-H" "Content-Type: application/json"
|
||||
"-H" (str "Authorization: Bearer " api-key)
|
||||
"-d" (str "@" tmp-file))]
|
||||
(let [args (concat ["curl" "-s" "-X" "POST"
|
||||
(str "https://api.unsandbox.com" endpoint)
|
||||
"-H" "Content-Type: application/json"]
|
||||
auth-headers
|
||||
["-d" (str "@" tmp-file)])
|
||||
{:keys [out]} (apply sh args)]
|
||||
(io/delete-file tmp-file true)
|
||||
out)))
|
||||
|
||||
(defn curl-get [api-key endpoint]
|
||||
(:out (sh "curl" "-s"
|
||||
(str "https://api.unsandbox.com" endpoint)
|
||||
"-H" (str "Authorization: Bearer " api-key))))
|
||||
(let [[public-key secret-key] (get-api-keys)
|
||||
auth-headers (build-auth-headers public-key secret-key "GET" endpoint "")
|
||||
args (concat ["curl" "-s"
|
||||
(str "https://api.unsandbox.com" endpoint)]
|
||||
auth-headers)]
|
||||
(:out (apply sh args))))
|
||||
|
||||
(defn curl-delete [api-key endpoint]
|
||||
(:out (sh "curl" "-s" "-X" "DELETE"
|
||||
(str "https://api.unsandbox.com" endpoint)
|
||||
"-H" (str "Authorization: Bearer " api-key))))
|
||||
(let [[public-key secret-key] (get-api-keys)
|
||||
auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "")
|
||||
args (concat ["curl" "-s" "-X" "DELETE"
|
||||
(str "https://api.unsandbox.com" endpoint)]
|
||||
auth-headers)]
|
||||
(:out (apply sh args))))
|
||||
|
||||
(defn curl-portal-post [api-key endpoint json-data]
|
||||
(let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json")]
|
||||
(let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json")
|
||||
[public-key secret-key] (get-api-keys)
|
||||
auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)]
|
||||
(spit tmp-file json-data)
|
||||
(let [{:keys [out]} (sh "curl" "-s" "-X" "POST"
|
||||
(str portal-base endpoint)
|
||||
"-H" "Content-Type: application/json"
|
||||
"-H" (str "Authorization: Bearer " api-key)
|
||||
"-d" (str "@" tmp-file))]
|
||||
(let [args (concat ["curl" "-s" "-X" "POST"
|
||||
(str portal-base endpoint)
|
||||
"-H" "Content-Type: application/json"]
|
||||
auth-headers
|
||||
["-d" (str "@" tmp-file)])
|
||||
{:keys [out]} (apply sh args)]
|
||||
(io/delete-file tmp-file true)
|
||||
out)))
|
||||
|
||||
|
|
|
|||
44
un.cob
44
un.cob
|
|
@ -55,6 +55,8 @@
|
|||
01 WS-FILENAME PIC X(256).
|
||||
01 WS-FILE-STATUS PIC XX.
|
||||
01 WS-API-KEY PIC X(256).
|
||||
01 WS-PUBLIC-KEY PIC X(256).
|
||||
01 WS-SECRET-KEY PIC X(256).
|
||||
01 WS-LANGUAGE PIC X(32).
|
||||
01 WS-EXTENSION PIC X(16).
|
||||
01 WS-CURL-CMD PIC X(4096).
|
||||
|
|
@ -259,16 +261,46 @@
|
|||
END-IF.
|
||||
|
||||
MAKE-EXECUTE-REQUEST.
|
||||
* Build curl command using shell
|
||||
STRING "curl -s -X POST https://api.unsandbox.com/execute "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " FUNCTION TRIM(WS-API-KEY)
|
||||
"' --data-binary @- -o /tmp/unsandbox_resp.json "
|
||||
"< <(jq -Rs '{language: """
|
||||
* Get public/secret keys with fallback
|
||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_PUBLIC_KEY".
|
||||
IF WS-PUBLIC-KEY NOT = SPACES
|
||||
ACCEPT WS-SECRET-KEY FROM ENVIRONMENT "UNSANDBOX_SECRET_KEY"
|
||||
IF WS-SECRET-KEY = SPACES
|
||||
DISPLAY "Error: UNSANDBOX_SECRET_KEY not set"
|
||||
UPON SYSERR
|
||||
MOVE 1 TO RETURN-CODE
|
||||
STOP RUN
|
||||
END-IF
|
||||
ELSE
|
||||
ACCEPT WS-PUBLIC-KEY FROM ENVIRONMENT "UNSANDBOX_API_KEY"
|
||||
IF WS-PUBLIC-KEY = SPACES
|
||||
DISPLAY "Error: UNSANDBOX_PUBLIC_KEY/SECRET_KEY or "
|
||||
"UNSANDBOX_API_KEY not set" UPON SYSERR
|
||||
MOVE 1 TO RETURN-CODE
|
||||
STOP RUN
|
||||
END-IF
|
||||
MOVE WS-PUBLIC-KEY TO WS-SECRET-KEY
|
||||
END-IF.
|
||||
|
||||
* Build curl command using shell with HMAC signature
|
||||
STRING "TS=$(date +%s); "
|
||||
"BODY=$(jq -Rs '{language: """
|
||||
FUNCTION TRIM(WS-LANGUAGE)
|
||||
""", code: .}' < '"
|
||||
FUNCTION TRIM(WS-FILENAME)
|
||||
"'); "
|
||||
"SIG=$(echo -n \"$TS:POST:/execute:$BODY\" | "
|
||||
"openssl dgst -sha256 -hmac '"
|
||||
FUNCTION TRIM(WS-SECRET-KEY)
|
||||
"' | cut -d' ' -f2); "
|
||||
"curl -s -X POST https://api.unsandbox.com/execute "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer "
|
||||
FUNCTION TRIM(WS-PUBLIC-KEY)
|
||||
"' "
|
||||
"-H 'X-Timestamp: '$TS "
|
||||
"-H 'X-Signature: '$SIG "
|
||||
"--data-binary \"$BODY\" -o /tmp/unsandbox_resp.json; "
|
||||
"jq -r '.stdout // empty' /tmp/unsandbox_resp.json | "
|
||||
"sed 's/^/\x1b[34m/' | sed 's/$/\x1b[0m/'; "
|
||||
"jq -r '.stderr // empty' /tmp/unsandbox_resp.json | "
|
||||
|
|
|
|||
111
un.cpp
111
un.cpp
|
|
@ -50,6 +50,7 @@
|
|||
#include <vector>
|
||||
#include <map>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <sys/stat.h>
|
||||
|
||||
using namespace std;
|
||||
|
|
@ -111,7 +112,37 @@ string exec_curl(const string& cmd) {
|
|||
return result;
|
||||
}
|
||||
|
||||
void cmd_execute(const string& source_file, const vector<string>& envs, const vector<string>& files, bool artifacts, const string& network, int vcpu, const string& api_key) {
|
||||
string compute_hmac(const string& secret_key, const string& message) {
|
||||
string cmd = "echo -n '" + message + "' | openssl dgst -sha256 -hmac '" + secret_key + "' -hex | sed 's/.*= //'";
|
||||
string result = exec_curl(cmd);
|
||||
// Trim newline
|
||||
while (!result.empty() && (result.back() == '\n' || result.back() == '\r')) {
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
string get_timestamp() {
|
||||
return to_string(time(nullptr));
|
||||
}
|
||||
|
||||
string build_auth_headers(const string& method, const string& path, const string& body, const string& public_key, const string& secret_key) {
|
||||
if (secret_key.empty()) {
|
||||
// Legacy mode: use public_key as bearer token
|
||||
return "-H 'Authorization: Bearer " + public_key + "'";
|
||||
}
|
||||
|
||||
// HMAC mode
|
||||
string timestamp = get_timestamp();
|
||||
string message = timestamp + ":" + method + ":" + path + ":" + body;
|
||||
string signature = compute_hmac(secret_key, message);
|
||||
|
||||
return "-H 'Authorization: Bearer " + public_key + "' "
|
||||
"-H 'X-Timestamp: " + timestamp + "' "
|
||||
"-H 'X-Signature: " + signature + "'";
|
||||
}
|
||||
|
||||
void cmd_execute(const string& source_file, const vector<string>& envs, const vector<string>& files, bool artifacts, const string& network, int vcpu, const string& public_key, const string& secret_key) {
|
||||
string lang = detect_language(source_file);
|
||||
if (lang.empty()) {
|
||||
cerr << RED << "Error: Cannot detect language" << RESET << endl;
|
||||
|
|
@ -140,9 +171,10 @@ void cmd_execute(const string& source_file, const vector<string>& envs, const ve
|
|||
if (vcpu > 0) json << ",\"vcpu\":" << vcpu;
|
||||
json << "}";
|
||||
|
||||
string auth_headers = build_auth_headers("POST", "/execute", json.str(), public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/execute' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "' "
|
||||
+ auth_headers + " "
|
||||
"-d '" + json.str() + "'";
|
||||
|
||||
string result = exec_curl(cmd);
|
||||
|
|
@ -188,15 +220,17 @@ 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& api_key) {
|
||||
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) {
|
||||
if (list) {
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/sessions' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("GET", "/sessions", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/sessions' " + auth_headers;
|
||||
cout << exec_curl(cmd) << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kill.empty()) {
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + "/sessions/" + kill + "' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("DELETE", "/sessions/" + kill, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + "/sessions/" + kill + "' " + auth_headers;
|
||||
exec_curl(cmd);
|
||||
cout << GREEN << "Session terminated: " << kill << RESET << endl;
|
||||
return;
|
||||
|
|
@ -211,54 +245,62 @@ void cmd_session(bool list, const string& kill, const string& shell, const strin
|
|||
json << "}";
|
||||
|
||||
cout << YELLOW << "Creating session..." << RESET << endl;
|
||||
string auth_headers = build_auth_headers("POST", "/sessions", json.str(), public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/sessions' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "' "
|
||||
+ auth_headers + " "
|
||||
"-d '" + json.str() + "'";
|
||||
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& api_key) {
|
||||
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) {
|
||||
if (list) {
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("GET", "/services", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services' " + auth_headers;
|
||||
cout << exec_curl(cmd) << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.empty()) {
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + info + "' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("GET", "/services/" + info, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + info + "' " + auth_headers;
|
||||
cout << exec_curl(cmd) << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!logs.empty()) {
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + logs + "/logs' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("GET", "/services/" + logs + "/logs", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + logs + "/logs' " + auth_headers;
|
||||
exec_curl(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tail.empty()) {
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + tail + "/logs?lines=9000' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("GET", "/services/" + tail + "/logs?lines=9000", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X GET '" + API_BASE + "/services/" + tail + "/logs?lines=9000' " + auth_headers;
|
||||
exec_curl(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sleep.empty()) {
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + sleep + "/sleep' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("POST", "/services/" + sleep + "/sleep", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + sleep + "/sleep' " + auth_headers;
|
||||
exec_curl(cmd);
|
||||
cout << GREEN << "Service sleeping: " << sleep << RESET << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wake.empty()) {
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + wake + "/wake' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("POST", "/services/" + wake + "/wake", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + wake + "/wake' " + auth_headers;
|
||||
exec_curl(cmd);
|
||||
cout << GREEN << "Service waking: " << wake << RESET << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!destroy.empty()) {
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + "/services/" + destroy + "' -H 'Authorization: Bearer " + api_key + "'";
|
||||
string auth_headers = build_auth_headers("DELETE", "/services/" + destroy, "", public_key, secret_key);
|
||||
string cmd = "curl -s -X DELETE '" + API_BASE + "/services/" + destroy + "' " + auth_headers;
|
||||
exec_curl(cmd);
|
||||
cout << GREEN << "Service destroyed: " << destroy << RESET << endl;
|
||||
return;
|
||||
|
|
@ -267,9 +309,10 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
if (!execute.empty()) {
|
||||
ostringstream json;
|
||||
json << "{\"command\":\"" << escape_json(command) << "\"}";
|
||||
string auth_headers = build_auth_headers("POST", "/services/" + execute + "/execute", json.str(), public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + execute + "/execute' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "' "
|
||||
+ auth_headers + " "
|
||||
"-d '" + json.str() + "'";
|
||||
string result = exec_curl(cmd);
|
||||
|
||||
|
|
@ -308,10 +351,12 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
|
||||
if (!dump_bootstrap.empty()) {
|
||||
cerr << "Fetching bootstrap script from " << dump_bootstrap << "..." << endl;
|
||||
string json_body = "{\"command\":\"cat /tmp/bootstrap.sh\"}";
|
||||
string auth_headers = build_auth_headers("POST", "/services/" + dump_bootstrap + "/execute", json_body, public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services/" + dump_bootstrap + "/execute' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "' "
|
||||
"-d '{\"command\":\"cat /tmp/bootstrap.sh\"}'";
|
||||
+ auth_headers + " "
|
||||
"-d '" + json_body + "'";
|
||||
string result = exec_curl(cmd);
|
||||
|
||||
size_t stdout_pos = result.find("\"stdout\":\"");
|
||||
|
|
@ -370,9 +415,10 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
json << "}";
|
||||
|
||||
cout << YELLOW << "Creating service..." << RESET << endl;
|
||||
string auth_headers = build_auth_headers("POST", "/services", json.str(), public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + API_BASE + "/services' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "' "
|
||||
+ auth_headers + " "
|
||||
"-d '" + json.str() + "'";
|
||||
cout << exec_curl(cmd) << endl;
|
||||
return;
|
||||
|
|
@ -382,10 +428,11 @@ void cmd_service(const string& name, const string& ports, const string& type, co
|
|||
exit(1);
|
||||
}
|
||||
|
||||
void cmd_validate_key(bool extend, const string& api_key) {
|
||||
void cmd_validate_key(bool extend, const string& public_key, const string& secret_key) {
|
||||
string auth_headers = build_auth_headers("POST", "/keys/validate", "", public_key, secret_key);
|
||||
string cmd = "curl -s -X POST '" + PORTAL_BASE + "/keys/validate' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer " + api_key + "'";
|
||||
+ auth_headers;
|
||||
|
||||
string result = exec_curl(cmd);
|
||||
|
||||
|
|
@ -464,7 +511,13 @@ void cmd_validate_key(bool extend, const string& api_key) {
|
|||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
string api_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : "";
|
||||
string public_key = getenv("UNSANDBOX_PUBLIC_KEY") ? getenv("UNSANDBOX_PUBLIC_KEY") : "";
|
||||
string secret_key = getenv("UNSANDBOX_SECRET_KEY") ? getenv("UNSANDBOX_SECRET_KEY") : "";
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if (public_key.empty()) {
|
||||
public_key = getenv("UNSANDBOX_API_KEY") ? getenv("UNSANDBOX_API_KEY") : "";
|
||||
}
|
||||
|
||||
if (argc < 2) {
|
||||
cerr << "Usage: " << argv[0] << " [options] <source_file>" << endl;
|
||||
|
|
@ -491,10 +544,10 @@ int main(int argc, char* argv[]) {
|
|||
else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
|
||||
else if (arg == "--tmux") tmux = true;
|
||||
else if (arg == "--screen") screen = true;
|
||||
else if (arg == "-k" && i+1 < argc) api_key = argv[++i];
|
||||
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, api_key);
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -523,10 +576,10 @@ int main(int argc, char* argv[]) {
|
|||
else if (arg == "--dump-file" && i+1 < argc) dump_file = 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 == "-k" && i+1 < argc) api_key = argv[++i];
|
||||
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, api_key);
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -536,10 +589,10 @@ int main(int argc, char* argv[]) {
|
|||
for (int i = 2; i < argc; i++) {
|
||||
string arg = argv[i];
|
||||
if (arg == "--extend") extend = true;
|
||||
else if (arg == "-k" && i+1 < argc) api_key = argv[++i];
|
||||
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_validate_key(extend, api_key);
|
||||
cmd_validate_key(extend, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -556,7 +609,7 @@ int main(int argc, char* argv[]) {
|
|||
else if (arg == "-a") artifacts = true;
|
||||
else if (arg == "-n" && i+1 < argc) network = argv[++i];
|
||||
else if (arg == "-v" && i+1 < argc) vcpu = stoi(argv[++i]);
|
||||
else if (arg == "-k" && i+1 < argc) api_key = argv[++i];
|
||||
else if (arg == "-k" && i+1 < argc) public_key = argv[++i];
|
||||
else if (arg[0] != '-') source_file = arg;
|
||||
}
|
||||
|
||||
|
|
@ -565,6 +618,6 @@ int main(int argc, char* argv[]) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
cmd_execute(source_file, envs, files, artifacts, network, vcpu, api_key);
|
||||
cmd_execute(source_file, envs, files, artifacts, network, vcpu, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
98
un.cr
98
un.cr
|
|
@ -41,6 +41,7 @@ require "http/client"
|
|||
require "json"
|
||||
require "base64"
|
||||
require "option_parser"
|
||||
require "openssl/hmac"
|
||||
|
||||
# Extension to language mapping
|
||||
EXT_MAP = {
|
||||
|
|
@ -74,28 +75,51 @@ def detect_language(filename : String) : String
|
|||
EXT_MAP.fetch(ext, "unknown")
|
||||
end
|
||||
|
||||
def get_api_key(args_key : String?) : String
|
||||
key = args_key || ENV["UNSANDBOX_API_KEY"]?
|
||||
if key.nil? || key.empty?
|
||||
STDERR.puts "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
|
||||
exit 1
|
||||
def get_api_keys(args_key : String?) : {String, String?}
|
||||
public_key = ENV["UNSANDBOX_PUBLIC_KEY"]?
|
||||
secret_key = ENV["UNSANDBOX_SECRET_KEY"]?
|
||||
|
||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if public_key.nil? || public_key.empty? || secret_key.nil? || secret_key.empty?
|
||||
legacy_key = args_key || ENV["UNSANDBOX_API_KEY"]?
|
||||
if legacy_key.nil? || legacy_key.empty?
|
||||
STDERR.puts "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
return {legacy_key, nil}
|
||||
end
|
||||
key
|
||||
|
||||
{public_key, secret_key}
|
||||
end
|
||||
|
||||
def api_request(endpoint : String, api_key : String, method = "GET", data : JSON::Any? = nil)
|
||||
def api_request(endpoint : String, public_key : String, secret_key : String?, method = "GET", data : JSON::Any? = nil)
|
||||
url = URI.parse(API_BASE + endpoint)
|
||||
headers = HTTP::Headers{
|
||||
"Content-Type" => "application/json",
|
||||
"Authorization" => "Bearer #{api_key}"
|
||||
"Content-Type" => "application/json"
|
||||
}
|
||||
|
||||
body = data ? data.to_json : ""
|
||||
|
||||
# Add HMAC authentication headers if secret_key is provided
|
||||
if secret_key && !secret_key.empty?
|
||||
timestamp = Time.utc.to_unix.to_s
|
||||
message = "#{timestamp}:#{method}:#{endpoint}:#{body}"
|
||||
|
||||
signature = OpenSSL::HMAC.hexdigest(:sha256, secret_key, message)
|
||||
|
||||
headers["Authorization"] = "Bearer #{public_key}"
|
||||
headers["X-Timestamp"] = timestamp
|
||||
headers["X-Signature"] = signature
|
||||
else
|
||||
# Legacy API key authentication
|
||||
headers["Authorization"] = "Bearer #{public_key}"
|
||||
end
|
||||
|
||||
begin
|
||||
response = case method
|
||||
when "GET"
|
||||
HTTP::Client.get(url, headers: headers)
|
||||
when "POST"
|
||||
body = data ? data.to_json : ""
|
||||
HTTP::Client.post(url, headers: headers, body: body)
|
||||
when "DELETE"
|
||||
HTTP::Client.delete(url, headers: headers)
|
||||
|
|
@ -112,7 +136,7 @@ def api_request(endpoint : String, api_key : String, method = "GET", data : JSON
|
|||
end
|
||||
|
||||
def cmd_execute(args)
|
||||
api_key = get_api_key(args[:api_key]?)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
filename = args[:source_file].as(String)
|
||||
unless File.exists?(filename)
|
||||
|
|
@ -173,7 +197,7 @@ def cmd_execute(args)
|
|||
end
|
||||
|
||||
# Execute
|
||||
result = api_request("/execute", api_key, method: "POST", data: payload)
|
||||
result = api_request("/execute", public_key, secret_key, method: "POST", data: payload)
|
||||
|
||||
# Print output
|
||||
if stdout = result["stdout"]?.try(&.as_s?)
|
||||
|
|
@ -202,10 +226,10 @@ def cmd_execute(args)
|
|||
end
|
||||
|
||||
def cmd_session(args)
|
||||
api_key = get_api_key(args[:api_key]?)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
if args[:list]?.as?(Bool)
|
||||
result = api_request("/sessions", api_key)
|
||||
result = api_request("/sessions", public_key, secret_key)
|
||||
sessions = result["sessions"]?.try(&.as_a?) || [] of JSON::Any
|
||||
if sessions.empty?
|
||||
puts "No active sessions"
|
||||
|
|
@ -223,7 +247,7 @@ def cmd_session(args)
|
|||
end
|
||||
|
||||
if kill_id = args[:kill]?.as?(String)
|
||||
api_request("/sessions/#{kill_id}", api_key, method: "DELETE")
|
||||
api_request("/sessions/#{kill_id}", public_key, secret_key, method: "DELETE")
|
||||
puts "#{GREEN}Session terminated: #{kill_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
|
@ -233,17 +257,33 @@ def cmd_session(args)
|
|||
end
|
||||
|
||||
def cmd_key(args)
|
||||
api_key = get_api_key(args[:api_key]?)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
# Validate key
|
||||
url = URI.parse(PORTAL_BASE + "/keys/validate")
|
||||
headers = HTTP::Headers{
|
||||
"Content-Type" => "application/json",
|
||||
"Authorization" => "Bearer #{api_key}"
|
||||
"Content-Type" => "application/json"
|
||||
}
|
||||
|
||||
body = "{}"
|
||||
|
||||
# Add HMAC authentication headers if secret_key is provided
|
||||
if secret_key && !secret_key.empty?
|
||||
timestamp = Time.utc.to_unix.to_s
|
||||
message = "#{timestamp}:POST:/keys/validate:#{body}"
|
||||
|
||||
signature = OpenSSL::HMAC.hexdigest(:sha256, secret_key, message)
|
||||
|
||||
headers["Authorization"] = "Bearer #{public_key}"
|
||||
headers["X-Timestamp"] = timestamp
|
||||
headers["X-Signature"] = signature
|
||||
else
|
||||
# Legacy API key authentication
|
||||
headers["Authorization"] = "Bearer #{public_key}"
|
||||
end
|
||||
|
||||
begin
|
||||
response = HTTP::Client.post(url, headers: headers, body: "{}")
|
||||
response = HTTP::Client.post(url, headers: headers, body: body)
|
||||
result = JSON.parse(response.body)
|
||||
|
||||
status = result["status"]?.try(&.as_s?) || "unknown"
|
||||
|
|
@ -311,10 +351,10 @@ def cmd_key(args)
|
|||
end
|
||||
|
||||
def cmd_service(args)
|
||||
api_key = get_api_key(args[:api_key]?)
|
||||
public_key, secret_key = get_api_keys(args[:api_key]?)
|
||||
|
||||
if args[:list]?.as?(Bool)
|
||||
result = api_request("/services", api_key)
|
||||
result = api_request("/services", public_key, secret_key)
|
||||
services = result["services"]?.try(&.as_a?) || [] of JSON::Any
|
||||
if services.empty?
|
||||
puts "No services"
|
||||
|
|
@ -334,31 +374,31 @@ def cmd_service(args)
|
|||
end
|
||||
|
||||
if info_id = args[:info]?.as?(String)
|
||||
result = api_request("/services/#{info_id}", api_key)
|
||||
result = api_request("/services/#{info_id}", public_key, secret_key)
|
||||
puts result.to_pretty_json
|
||||
return
|
||||
end
|
||||
|
||||
if logs_id = args[:logs]?.as?(String)
|
||||
result = api_request("/services/#{logs_id}/logs", api_key)
|
||||
result = api_request("/services/#{logs_id}/logs", public_key, secret_key)
|
||||
puts result["logs"]?.try(&.as_s?) || ""
|
||||
return
|
||||
end
|
||||
|
||||
if sleep_id = args[:sleep]?.as?(String)
|
||||
api_request("/services/#{sleep_id}/sleep", api_key, method: "POST")
|
||||
api_request("/services/#{sleep_id}/sleep", public_key, secret_key, method: "POST")
|
||||
puts "#{GREEN}Service sleeping: #{sleep_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if wake_id = args[:wake]?.as?(String)
|
||||
api_request("/services/#{wake_id}/wake", api_key, method: "POST")
|
||||
api_request("/services/#{wake_id}/wake", public_key, secret_key, method: "POST")
|
||||
puts "#{GREEN}Service waking: #{wake_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if destroy_id = args[:destroy]?.as?(String)
|
||||
api_request("/services/#{destroy_id}", api_key, method: "DELETE")
|
||||
api_request("/services/#{destroy_id}", public_key, secret_key, method: "DELETE")
|
||||
puts "#{GREEN}Service destroyed: #{destroy_id}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
|
@ -366,7 +406,7 @@ def cmd_service(args)
|
|||
if execute_id = args[:execute]?.as?(String)
|
||||
command = args[:command]?.as?(String) || ""
|
||||
payload = JSON.parse({command: command}.to_json)
|
||||
result = api_request("/services/#{execute_id}/execute", api_key, method: "POST", data: payload)
|
||||
result = api_request("/services/#{execute_id}/execute", public_key, secret_key, method: "POST", data: payload)
|
||||
if stdout = result["stdout"]?.try(&.as_s?)
|
||||
print BLUE, stdout, RESET
|
||||
end
|
||||
|
|
@ -379,7 +419,7 @@ def cmd_service(args)
|
|||
if dump_id = args[:dump_bootstrap]?.as?(String)
|
||||
STDERR.puts "Fetching bootstrap script from #{dump_id}..."
|
||||
payload = JSON.parse({command: "cat /tmp/bootstrap.sh"}.to_json)
|
||||
result = api_request("/services/#{dump_id}/execute", api_key, method: "POST", data: payload)
|
||||
result = api_request("/services/#{dump_id}/execute", public_key, secret_key, method: "POST", data: payload)
|
||||
|
||||
if bootstrap = result["stdout"]?.try(&.as_s?)
|
||||
if file_path = args[:dump_file]?.as?(String)
|
||||
|
|
@ -433,7 +473,7 @@ def cmd_service(args)
|
|||
end
|
||||
|
||||
# Create service
|
||||
result = api_request("/services", api_key, method: "POST", data: payload)
|
||||
result = api_request("/services", public_key, secret_key, method: "POST", data: payload)
|
||||
puts "#{GREEN}Service created: #{result["id"]?.try(&.as_s?) || "N/A"}#{RESET}"
|
||||
puts "Name: #{result["name"]?.try(&.as_s?) || "N/A"}"
|
||||
if url = result["url"]?.try(&.as_s?)
|
||||
|
|
|
|||
122
un.d
122
un.d
|
|
@ -88,12 +88,43 @@ string escapeJson(string s) {
|
|||
return result;
|
||||
}
|
||||
|
||||
string computeHmac(string secretKey, string message) {
|
||||
import std.process : pipeShell, Redirect, wait;
|
||||
import std.stdio : File;
|
||||
|
||||
auto cmd = format("echo -n '%s' | openssl dgst -sha256 -hmac '%s' -hex 2>/dev/null | sed 's/.*= //'", message, secretKey);
|
||||
auto pipes = pipeShell(cmd, Redirect.stdout);
|
||||
string result = pipes.stdout.readln().strip();
|
||||
wait(pipes.pid);
|
||||
return result;
|
||||
}
|
||||
|
||||
string getTimestamp() {
|
||||
import std.datetime.systime : Clock;
|
||||
return format("%d", Clock.currTime.toUnixTime());
|
||||
}
|
||||
|
||||
string buildAuthHeaders(string method, string path, string body, string publicKey, string secretKey) {
|
||||
if (secretKey.empty) {
|
||||
// Legacy mode: use public_key as bearer token
|
||||
return format("-H 'Authorization: Bearer %s'", publicKey);
|
||||
}
|
||||
|
||||
// HMAC mode
|
||||
string timestamp = getTimestamp();
|
||||
string message = format("%s:%s:%s:%s", timestamp, method, path, body);
|
||||
string signature = computeHmac(secretKey, message);
|
||||
|
||||
return format("-H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'",
|
||||
publicKey, timestamp, signature);
|
||||
}
|
||||
|
||||
string execCurl(string cmd) {
|
||||
auto result = executeShell(cmd);
|
||||
return result.output;
|
||||
}
|
||||
|
||||
void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network, int vcpu, string apiKey) {
|
||||
void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network, int vcpu, string publicKey, string secretKey) {
|
||||
string lang = detectLanguage(sourceFile);
|
||||
if (lang.empty) {
|
||||
stderr.writefln("%sError: Cannot detect language%s", RED, RESET);
|
||||
|
|
@ -120,21 +151,25 @@ void cmdExecute(string sourceFile, string[] envs, bool artifacts, string network
|
|||
if (vcpu > 0) json ~= format(`,"vcpu":%d`, vcpu);
|
||||
json ~= "}";
|
||||
|
||||
string cmd = format(`curl -s -X POST '%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, apiKey, json);
|
||||
string authHeaders = buildAuthHeaders("POST", "/execute", json, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json);
|
||||
string result = execCurl(cmd);
|
||||
|
||||
writeln(result);
|
||||
}
|
||||
|
||||
void cmdSession(bool list, string kill, string shell, string network, int vcpu, bool tmux, bool screen, string apiKey) {
|
||||
void cmdSession(bool list, string kill, string shell, string network, int vcpu, bool tmux, bool screen, string publicKey, string secretKey) {
|
||||
if (list) {
|
||||
string cmd = format(`curl -s -X GET '%s/sessions' -H 'Authorization: Bearer %s'`, API_BASE, apiKey);
|
||||
string authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/sessions' %s`, API_BASE, authHeaders);
|
||||
writeln(execCurl(cmd));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kill.empty) {
|
||||
string cmd = format(`curl -s -X DELETE '%s/sessions/%s' -H 'Authorization: Bearer %s'`, API_BASE, kill, apiKey);
|
||||
string path = format("/sessions/%s", kill);
|
||||
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X DELETE '%s/sessions/%s' %s`, API_BASE, kill, authHeaders);
|
||||
execCurl(cmd);
|
||||
writefln("%sSession terminated: %s%s", GREEN, kill, RESET);
|
||||
return;
|
||||
|
|
@ -148,51 +183,65 @@ void cmdSession(bool list, string kill, string shell, string network, int vcpu,
|
|||
json ~= "}";
|
||||
|
||||
writefln("%sCreating session...%s", YELLOW, RESET);
|
||||
string cmd = format(`curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, apiKey, json);
|
||||
string authHeaders = buildAuthHeaders("POST", "/sessions", json, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json);
|
||||
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 apiKey) {
|
||||
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) {
|
||||
if (list) {
|
||||
string cmd = format(`curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'`, API_BASE, apiKey);
|
||||
string authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/services' %s`, API_BASE, authHeaders);
|
||||
writeln(execCurl(cmd));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.empty) {
|
||||
string cmd = format(`curl -s -X GET '%s/services/%s' -H 'Authorization: Bearer %s'`, API_BASE, info, apiKey);
|
||||
string path = format("/services/%s", info);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/services/%s' %s`, API_BASE, info, authHeaders);
|
||||
writeln(execCurl(cmd));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!logs.empty) {
|
||||
string cmd = format(`curl -s -X GET '%s/services/%s/logs' -H 'Authorization: Bearer %s'`, API_BASE, logs, apiKey);
|
||||
string path = format("/services/%s/logs", logs);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/services/%s/logs' %s`, API_BASE, logs, authHeaders);
|
||||
write(execCurl(cmd));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tail.empty) {
|
||||
string cmd = format(`curl -s -X GET '%s/services/%s/logs?lines=9000' -H 'Authorization: Bearer %s'`, API_BASE, tail, apiKey);
|
||||
string path = format("/services/%s/logs", tail);
|
||||
string authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X GET '%s/services/%s/logs?lines=9000' %s`, API_BASE, tail, authHeaders);
|
||||
write(execCurl(cmd));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sleep.empty) {
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/sleep' -H 'Authorization: Bearer %s'`, API_BASE, sleep, apiKey);
|
||||
string path = format("/services/%s/sleep", sleep);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/sleep' %s`, API_BASE, sleep, authHeaders);
|
||||
execCurl(cmd);
|
||||
writefln("%sService sleeping: %s%s", GREEN, sleep, RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wake.empty) {
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/wake' -H 'Authorization: Bearer %s'`, API_BASE, wake, apiKey);
|
||||
string path = format("/services/%s/wake", wake);
|
||||
string authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/wake' %s`, API_BASE, wake, authHeaders);
|
||||
execCurl(cmd);
|
||||
writefln("%sService waking: %s%s", GREEN, wake, RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!destroy.empty) {
|
||||
string cmd = format(`curl -s -X DELETE '%s/services/%s' -H 'Authorization: Bearer %s'`, API_BASE, destroy, apiKey);
|
||||
string path = format("/services/%s", destroy);
|
||||
string authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X DELETE '%s/services/%s' %s`, API_BASE, destroy, authHeaders);
|
||||
execCurl(cmd);
|
||||
writefln("%sService destroyed: %s%s", GREEN, destroy, RESET);
|
||||
return;
|
||||
|
|
@ -200,7 +249,9 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
|
|||
|
||||
if (!execute.empty) {
|
||||
string json = format(`{"command":"%s"}`, escapeJson(command));
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, execute, apiKey, json);
|
||||
string path = format("/services/%s/execute", execute);
|
||||
string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, execute, authHeaders, json);
|
||||
string result = execCurl(cmd);
|
||||
|
||||
// Simple JSON parsing for stdout/stderr
|
||||
|
|
@ -229,7 +280,10 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
|
|||
|
||||
if (!dumpBootstrap.empty) {
|
||||
stderr.writefln("Fetching bootstrap script from %s...", dumpBootstrap);
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{"command":"cat /tmp/bootstrap.sh"}'`, API_BASE, dumpBootstrap, apiKey);
|
||||
string json = `{"command":"cat /tmp/bootstrap.sh"}`;
|
||||
string path = format("/services/%s/execute", dumpBootstrap);
|
||||
string authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/services/%s/execute' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, dumpBootstrap, authHeaders, json);
|
||||
string result = execCurl(cmd);
|
||||
|
||||
import std.algorithm : findSplitAfter;
|
||||
|
|
@ -283,7 +337,8 @@ void cmdService(string name, string ports, string bootstrap, string type, bool l
|
|||
json ~= "}";
|
||||
|
||||
writefln("%sCreating service...%s", YELLOW, RESET);
|
||||
string cmd = format(`curl -s -X POST '%s/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'`, API_BASE, apiKey, json);
|
||||
string authHeaders = buildAuthHeaders("POST", "/services", json, publicKey, secretKey);
|
||||
string cmd = format(`curl -s -X POST '%s/services' -H 'Content-Type: application/json' %s -d '%s'`, API_BASE, authHeaders, json);
|
||||
writeln(execCurl(cmd));
|
||||
return;
|
||||
}
|
||||
|
|
@ -318,11 +373,12 @@ string formatDuration(long totalMinutes) {
|
|||
}
|
||||
}
|
||||
|
||||
void validateKey(string apiKey, bool extend) {
|
||||
void validateKey(string publicKey, string secretKey, bool extend) {
|
||||
import std.json;
|
||||
import std.datetime;
|
||||
|
||||
string cmd = format(`curl -s -w '\n%%{http_code}' -X POST '%s/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer %s'`, PORTAL_BASE, apiKey);
|
||||
string authHeaders = buildAuthHeaders("POST", "/keys/validate", "", publicKey, secretKey);
|
||||
string cmd = format(`curl -s -w '\n%%{http_code}' -X POST '%s/keys/validate' -H 'Content-Type: application/json' %s`, PORTAL_BASE, authHeaders);
|
||||
string response = execCurl(cmd);
|
||||
|
||||
auto lines = response.split("\n");
|
||||
|
|
@ -413,7 +469,13 @@ void validateKey(string apiKey, bool extend) {
|
|||
}
|
||||
|
||||
int main(string[] args) {
|
||||
string apiKey = environment.get("UNSANDBOX_API_KEY", "");
|
||||
string publicKey = environment.get("UNSANDBOX_PUBLIC_KEY", "");
|
||||
string secretKey = environment.get("UNSANDBOX_SECRET_KEY", "");
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if (publicKey.empty) {
|
||||
publicKey = environment.get("UNSANDBOX_API_KEY", "");
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
stderr.writefln("Usage: %s [options] <source_file>", args[0]);
|
||||
|
|
@ -437,10 +499,10 @@ 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] == "-k" && i+1 < args.length) apiKey = args[++i];
|
||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
|
||||
}
|
||||
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, apiKey);
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -468,10 +530,10 @@ 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] == "-k" && i+1 < args.length) apiKey = 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, apiKey);
|
||||
cmdService(name, ports, bootstrap, type, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -480,15 +542,15 @@ int main(string[] args) {
|
|||
|
||||
for (size_t i = 2; i < args.length; i++) {
|
||||
if (args[i] == "--extend") extend = true;
|
||||
else if (args[i] == "-k" && i+1 < args.length) apiKey = args[++i];
|
||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
|
||||
}
|
||||
|
||||
if (apiKey.empty) {
|
||||
stderr.writefln("%sError: UNSANDBOX_API_KEY not set%s", RED, RESET);
|
||||
if (publicKey.empty) {
|
||||
stderr.writefln("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s", RED, RESET);
|
||||
return 1;
|
||||
}
|
||||
|
||||
validateKey(apiKey, extend);
|
||||
validateKey(publicKey, secretKey, extend);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -503,7 +565,7 @@ int main(string[] args) {
|
|||
else if (args[i] == "-a") artifacts = true;
|
||||
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] == "-k" && i+1 < args.length) apiKey = args[++i];
|
||||
else if (args[i] == "-k" && i+1 < args.length) publicKey = args[++i];
|
||||
else if (!args[i].startsWith("-")) sourceFile = args[i];
|
||||
}
|
||||
|
||||
|
|
@ -512,6 +574,6 @@ int main(string[] args) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
cmdExecute(sourceFile, envs, artifacts, network, vcpu, apiKey);
|
||||
cmdExecute(sourceFile, envs, artifacts, network, vcpu, publicKey, secretKey);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
92
un.dart
92
un.dart
|
|
@ -42,6 +42,7 @@
|
|||
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
const String apiBase = 'https://api.unsandbox.com';
|
||||
const String portalBase = 'https://unsandbox.com';
|
||||
|
|
@ -98,13 +99,21 @@ class Args {
|
|||
bool keyExtend = false;
|
||||
}
|
||||
|
||||
String getApiKey(String? argsKey) {
|
||||
final key = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
|
||||
if (key == null || key.isEmpty) {
|
||||
stderr.writeln('${red}Error: UNSANDBOX_API_KEY not set$reset');
|
||||
exit(1);
|
||||
List<String?> getApiKeys(String? argsKey) {
|
||||
final publicKey = Platform.environment['UNSANDBOX_PUBLIC_KEY'];
|
||||
final secretKey = Platform.environment['UNSANDBOX_SECRET_KEY'];
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if (publicKey == null || publicKey.isEmpty || secretKey == null || secretKey.isEmpty) {
|
||||
final legacyKey = argsKey ?? Platform.environment['UNSANDBOX_API_KEY'];
|
||||
if (legacyKey == null || legacyKey.isEmpty) {
|
||||
stderr.writeln('${red}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set$reset');
|
||||
exit(1);
|
||||
}
|
||||
return [legacyKey, null];
|
||||
}
|
||||
return key;
|
||||
|
||||
return [publicKey, secretKey];
|
||||
}
|
||||
|
||||
String detectLanguage(String filename) {
|
||||
|
|
@ -120,18 +129,37 @@ String detectLanguage(String filename) {
|
|||
return lang;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String apiKey, {String? baseUrl}) async {
|
||||
Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, String? jsonData, String publicKey, String? secretKey, {String? baseUrl}) async {
|
||||
final base = baseUrl ?? apiBase;
|
||||
final tempFile = await File('${Directory.systemTemp.path}/un_request_${DateTime.now().millisecondsSinceEpoch}.json').create();
|
||||
|
||||
try {
|
||||
final body = jsonData ?? '';
|
||||
if (jsonData != null) {
|
||||
await tempFile.writeAsString(jsonData);
|
||||
}
|
||||
|
||||
final args = ['curl', '-s', '-X', method, '$base$endpoint',
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-H', 'Authorization: Bearer $apiKey'];
|
||||
'-H', 'Content-Type: application/json'];
|
||||
|
||||
// Add HMAC authentication headers if secretKey is provided
|
||||
if (secretKey != null && secretKey.isNotEmpty) {
|
||||
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
|
||||
final message = '$timestamp:$method:$endpoint:$body';
|
||||
|
||||
final key = utf8.encode(secretKey);
|
||||
final bytes = utf8.encode(message);
|
||||
final hmacSha256 = Hmac(sha256, key);
|
||||
final digest = hmacSha256.convert(bytes);
|
||||
final signature = digest.toString();
|
||||
|
||||
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
|
||||
args.addAll(['-H', 'X-Timestamp: $timestamp']);
|
||||
args.addAll(['-H', 'X-Signature: $signature']);
|
||||
} else {
|
||||
// Legacy API key authentication
|
||||
args.addAll(['-H', 'Authorization: Bearer $publicKey']);
|
||||
}
|
||||
|
||||
if (jsonData != null) {
|
||||
args.addAll(['-d', '@${tempFile.path}']);
|
||||
|
|
@ -151,7 +179,9 @@ Future<Map<String, dynamic>> apiRequestCurl(String endpoint, String method, Stri
|
|||
}
|
||||
|
||||
Future<void> cmdExecute(Args args) async {
|
||||
final apiKey = getApiKey(args.apiKey);
|
||||
final keys = getApiKeys(args.apiKey);
|
||||
final publicKey = keys[0]!;
|
||||
final secretKey = keys[1];
|
||||
final code = await File(args.sourceFile!).readAsString();
|
||||
final language = detectLanguage(args.sourceFile!);
|
||||
|
||||
|
|
@ -195,7 +225,7 @@ Future<void> cmdExecute(Args args) async {
|
|||
payload['vcpu'] = args.vcpu;
|
||||
}
|
||||
|
||||
final result = await apiRequestCurl('/execute', 'POST', jsonEncode(payload), apiKey);
|
||||
final result = await apiRequestCurl('/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
|
||||
final stdoutText = result['stdout'] as String?;
|
||||
final stderrText = result['stderr'] as String?;
|
||||
|
|
@ -227,10 +257,12 @@ Future<void> cmdExecute(Args args) async {
|
|||
}
|
||||
|
||||
Future<void> cmdSession(Args args) async {
|
||||
final apiKey = getApiKey(args.apiKey);
|
||||
final keys = getApiKeys(args.apiKey);
|
||||
final publicKey = keys[0]!;
|
||||
final secretKey = keys[1];
|
||||
|
||||
if (args.sessionList) {
|
||||
final result = await apiRequestCurl('/sessions', 'GET', null, apiKey);
|
||||
final result = await apiRequestCurl('/sessions', 'GET', null, publicKey, secretKey);
|
||||
final sessions = result['sessions'] as List? ?? [];
|
||||
if (sessions.isEmpty) {
|
||||
print('No active sessions');
|
||||
|
|
@ -245,7 +277,7 @@ Future<void> cmdSession(Args args) async {
|
|||
}
|
||||
|
||||
if (args.sessionKill != null) {
|
||||
await apiRequestCurl('/sessions/${args.sessionKill}', 'DELETE', null, apiKey);
|
||||
await apiRequestCurl('/sessions/${args.sessionKill}', 'DELETE', null, publicKey, secretKey);
|
||||
print('${green}Session terminated: ${args.sessionKill}$reset');
|
||||
return;
|
||||
}
|
||||
|
|
@ -261,16 +293,18 @@ Future<void> cmdSession(Args args) async {
|
|||
}
|
||||
|
||||
print('${yellow}Creating session...$reset');
|
||||
final result = await apiRequestCurl('/sessions', 'POST', jsonEncode(payload), apiKey);
|
||||
final result = await apiRequestCurl('/sessions', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
print('${green}Session created: ${result['id'] ?? 'N/A'}$reset');
|
||||
print('${yellow}(Interactive sessions require WebSocket - use un2 for full support)$reset');
|
||||
}
|
||||
|
||||
Future<void> cmdService(Args args) async {
|
||||
final apiKey = getApiKey(args.apiKey);
|
||||
final keys = getApiKeys(args.apiKey);
|
||||
final publicKey = keys[0]!;
|
||||
final secretKey = keys[1];
|
||||
|
||||
if (args.serviceList) {
|
||||
final result = await apiRequestCurl('/services', 'GET', null, apiKey);
|
||||
final result = await apiRequestCurl('/services', 'GET', null, publicKey, secretKey);
|
||||
final services = result['services'] as List? ?? [];
|
||||
if (services.isEmpty) {
|
||||
print('No services');
|
||||
|
|
@ -287,37 +321,37 @@ Future<void> cmdService(Args args) async {
|
|||
}
|
||||
|
||||
if (args.serviceInfo != null) {
|
||||
final result = await apiRequestCurl('/services/${args.serviceInfo}', 'GET', null, apiKey);
|
||||
final result = await apiRequestCurl('/services/${args.serviceInfo}', 'GET', null, publicKey, secretKey);
|
||||
print(jsonEncode(result));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceLogs != null) {
|
||||
final result = await apiRequestCurl('/services/${args.serviceLogs}/logs', 'GET', null, apiKey);
|
||||
final result = await apiRequestCurl('/services/${args.serviceLogs}/logs', 'GET', null, publicKey, secretKey);
|
||||
print(result['logs'] ?? '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceTail != null) {
|
||||
final result = await apiRequestCurl('/services/${args.serviceTail}/logs?lines=9000', 'GET', null, apiKey);
|
||||
final result = await apiRequestCurl('/services/${args.serviceTail}/logs?lines=9000', 'GET', null, publicKey, secretKey);
|
||||
print(result['logs'] ?? '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceSleep != null) {
|
||||
await apiRequestCurl('/services/${args.serviceSleep}/sleep', 'POST', null, apiKey);
|
||||
await apiRequestCurl('/services/${args.serviceSleep}/sleep', 'POST', null, publicKey, secretKey);
|
||||
print('${green}Service sleeping: ${args.serviceSleep}$reset');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceWake != null) {
|
||||
await apiRequestCurl('/services/${args.serviceWake}/wake', 'POST', null, apiKey);
|
||||
await apiRequestCurl('/services/${args.serviceWake}/wake', 'POST', null, publicKey, secretKey);
|
||||
print('${green}Service waking: ${args.serviceWake}$reset');
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.serviceDestroy != null) {
|
||||
await apiRequestCurl('/services/${args.serviceDestroy}', 'DELETE', null, apiKey);
|
||||
await apiRequestCurl('/services/${args.serviceDestroy}', 'DELETE', null, publicKey, secretKey);
|
||||
print('${green}Service destroyed: ${args.serviceDestroy}$reset');
|
||||
return;
|
||||
}
|
||||
|
|
@ -326,7 +360,7 @@ Future<void> cmdService(Args args) async {
|
|||
final payload = <String, dynamic>{
|
||||
'command': args.serviceCommand,
|
||||
};
|
||||
final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), apiKey);
|
||||
final result = await apiRequestCurl('/services/${args.serviceExecute}/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
final stdoutText = result['stdout'] as String?;
|
||||
final stderrText = result['stderr'] as String?;
|
||||
if (stdoutText != null && stdoutText.isNotEmpty) {
|
||||
|
|
@ -343,7 +377,7 @@ Future<void> cmdService(Args args) async {
|
|||
final payload = <String, dynamic>{
|
||||
'command': 'cat /tmp/bootstrap.sh',
|
||||
};
|
||||
final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), apiKey);
|
||||
final result = await apiRequestCurl('/services/${args.serviceDumpBootstrap}/execute', 'POST', jsonEncode(payload), publicKey, secretKey);
|
||||
|
||||
final bootstrap = result['stdout'] as String?;
|
||||
if (bootstrap != null && bootstrap.isNotEmpty) {
|
||||
|
|
@ -386,7 +420,7 @@ Future<void> cmdService(Args args) async {
|
|||
payload['vcpu'] = args.vcpu;
|
||||
}
|
||||
|
||||
final result = await apiRequestCurl('/services', 'POST', jsonEncode(payload), apiKey);
|
||||
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'}');
|
||||
if (result.containsKey('url')) {
|
||||
|
|
@ -400,10 +434,12 @@ Future<void> cmdService(Args args) async {
|
|||
}
|
||||
|
||||
Future<void> cmdKey(Args args) async {
|
||||
final apiKey = getApiKey(args.apiKey);
|
||||
final keys = getApiKeys(args.apiKey);
|
||||
final publicKey = keys[0]!;
|
||||
final secretKey = keys[1];
|
||||
|
||||
try {
|
||||
final result = await apiRequestCurl('/keys/validate', 'POST', null, apiKey, baseUrl: portalBase);
|
||||
final result = await apiRequestCurl('/keys/validate', 'POST', null, publicKey, secretKey, baseUrl: portalBase);
|
||||
|
||||
// Handle --extend flag
|
||||
if (args.keyExtend) {
|
||||
|
|
|
|||
60
un.erl
60
un.erl
|
|
@ -294,12 +294,44 @@ open_extend_page(PublicKey) ->
|
|||
end.
|
||||
|
||||
%% Helpers
|
||||
get_api_keys() ->
|
||||
PublicKey = os:getenv("UNSANDBOX_PUBLIC_KEY"),
|
||||
SecretKey = os:getenv("UNSANDBOX_SECRET_KEY"),
|
||||
ApiKey = os:getenv("UNSANDBOX_API_KEY"),
|
||||
|
||||
if
|
||||
PublicKey =/= false andalso SecretKey =/= false ->
|
||||
{PublicKey, SecretKey};
|
||||
ApiKey =/= false ->
|
||||
{ApiKey, false};
|
||||
true ->
|
||||
io:format("Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~n"),
|
||||
halt(1)
|
||||
end.
|
||||
|
||||
get_api_key() ->
|
||||
case os:getenv("UNSANDBOX_API_KEY") of
|
||||
false ->
|
||||
io:format("Error: UNSANDBOX_API_KEY not set~n"),
|
||||
halt(1);
|
||||
Key -> Key
|
||||
{PublicKey, _} = get_api_keys(),
|
||||
PublicKey.
|
||||
|
||||
hmac_sha256(Secret, Message) ->
|
||||
string:lowercase(
|
||||
lists:flatten([io_lib:format("~2.16.0b", [X]) || X <- binary_to_list(crypto:mac(hmac, sha256, Secret, Message))])
|
||||
).
|
||||
|
||||
make_signature(SecretKey, Timestamp, Method, Path, Body) ->
|
||||
Message = Timestamp ++ ":" ++ Method ++ ":" ++ Path ++ ":" ++ Body,
|
||||
hmac_sha256(SecretKey, Message).
|
||||
|
||||
build_auth_headers(PublicKey, SecretKey, Method, Path, Body) ->
|
||||
if
|
||||
SecretKey =/= false ->
|
||||
Timestamp = integer_to_list(erlang:system_time(second)),
|
||||
Signature = make_signature(SecretKey, Timestamp, Method, Path, Body),
|
||||
" -H 'Authorization: Bearer " ++ PublicKey ++ "'"
|
||||
++ " -H 'X-Timestamp: " ++ Timestamp ++ "'"
|
||||
++ " -H 'X-Signature: " ++ Signature ++ "'";
|
||||
true ->
|
||||
" -H 'Authorization: Bearer " ++ PublicKey ++ "'"
|
||||
end.
|
||||
|
||||
ext_to_lang(".hs") -> {ok, "haskell"};
|
||||
|
|
@ -364,30 +396,40 @@ write_temp_file(Data) ->
|
|||
TmpFile.
|
||||
|
||||
curl_post(ApiKey, Endpoint, TmpFile) ->
|
||||
{ok, Body} = file:read_file(TmpFile),
|
||||
BodyStr = binary_to_list(Body),
|
||||
{PublicKey, SecretKey} = get_api_keys(),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "POST", Endpoint, BodyStr),
|
||||
Cmd = "curl -s -X POST https://api.unsandbox.com" ++ Endpoint ++
|
||||
" -H 'Content-Type: application/json'" ++
|
||||
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
|
||||
AuthHeaders ++
|
||||
" -d @" ++ TmpFile,
|
||||
os:cmd(Cmd).
|
||||
|
||||
curl_post_portal(ApiKey, Endpoint, Data) ->
|
||||
TmpFile = write_temp_file(Data),
|
||||
{PublicKey, SecretKey} = get_api_keys(),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "POST", Endpoint, Data),
|
||||
Cmd = "curl -s -X POST https://unsandbox.com" ++ Endpoint ++
|
||||
" -H 'Content-Type: application/json'" ++
|
||||
" -H 'Authorization: Bearer " ++ ApiKey ++ "'" ++
|
||||
AuthHeaders ++
|
||||
" -d @" ++ TmpFile,
|
||||
Result = os:cmd(Cmd),
|
||||
file:delete(TmpFile),
|
||||
Result.
|
||||
|
||||
curl_get(ApiKey, Endpoint) ->
|
||||
{PublicKey, SecretKey} = get_api_keys(),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "GET", Endpoint, ""),
|
||||
Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++
|
||||
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
|
||||
AuthHeaders,
|
||||
os:cmd(Cmd).
|
||||
|
||||
curl_delete(ApiKey, Endpoint) ->
|
||||
{PublicKey, SecretKey} = get_api_keys(),
|
||||
AuthHeaders = build_auth_headers(PublicKey, SecretKey, "DELETE", Endpoint, ""),
|
||||
Cmd = "curl -s -X DELETE https://api.unsandbox.com" ++ Endpoint ++
|
||||
" -H 'Authorization: Bearer " ++ ApiKey ++ "'",
|
||||
AuthHeaders,
|
||||
os:cmd(Cmd).
|
||||
|
||||
%% Argument parsing
|
||||
|
|
|
|||
99
un.ex
99
un.ex
|
|
@ -401,15 +401,39 @@ defmodule Un do
|
|||
end
|
||||
|
||||
# Helpers
|
||||
defp get_api_key do
|
||||
case System.get_env("UNSANDBOX_API_KEY") do
|
||||
nil ->
|
||||
IO.puts(:stderr, "Error: UNSANDBOX_API_KEY not set")
|
||||
defp get_api_keys do
|
||||
public_key = System.get_env("UNSANDBOX_PUBLIC_KEY")
|
||||
secret_key = System.get_env("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
api_key = System.get_env("UNSANDBOX_API_KEY")
|
||||
|
||||
cond do
|
||||
public_key && secret_key ->
|
||||
{public_key, secret_key}
|
||||
api_key ->
|
||||
{api_key, nil}
|
||||
true ->
|
||||
IO.puts(:stderr, "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)")
|
||||
System.halt(1)
|
||||
key -> key
|
||||
end
|
||||
end
|
||||
|
||||
defp get_api_key do
|
||||
{public_key, _} = get_api_keys()
|
||||
public_key
|
||||
end
|
||||
|
||||
defp hmac_sha256(secret, message) do
|
||||
:crypto.mac(:hmac, :sha256, secret, message)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
|
||||
defp make_signature(secret_key, timestamp, method, path, body) do
|
||||
message = "#{timestamp}:#{method}:#{path}:#{body}"
|
||||
hmac_sha256(secret_key, message)
|
||||
end
|
||||
|
||||
defp escape_json(s) do
|
||||
s
|
||||
|> String.replace("\\", "\\\\")
|
||||
|
|
@ -427,50 +451,79 @@ defmodule Un do
|
|||
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
|
||||
File.write!(tmp_file, json)
|
||||
|
||||
{output, _exit} = System.cmd("curl", [
|
||||
{public_key, secret_key} = get_api_keys()
|
||||
headers = build_auth_headers(public_key, secret_key, "POST", endpoint, json)
|
||||
|
||||
args = [
|
||||
"-s", "-X", "POST",
|
||||
"https://api.unsandbox.com#{endpoint}",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-H", "Authorization: Bearer #{api_key}",
|
||||
"-d", "@#{tmp_file}"
|
||||
], stderr_to_stdout: true)
|
||||
"-H", "Content-Type: application/json"
|
||||
] ++ headers ++ ["-d", "@#{tmp_file}"]
|
||||
|
||||
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
|
||||
|
||||
File.rm(tmp_file)
|
||||
output
|
||||
end
|
||||
|
||||
defp build_auth_headers(public_key, secret_key, method, path, body) do
|
||||
if secret_key do
|
||||
timestamp = System.system_time(:second) |> Integer.to_string()
|
||||
signature = make_signature(secret_key, timestamp, method, path, body)
|
||||
[
|
||||
"-H", "Authorization: Bearer #{public_key}",
|
||||
"-H", "X-Timestamp: #{timestamp}",
|
||||
"-H", "X-Signature: #{signature}"
|
||||
]
|
||||
else
|
||||
# Backwards compatibility: use simple bearer token
|
||||
["-H", "Authorization: Bearer #{public_key}"]
|
||||
end
|
||||
end
|
||||
|
||||
defp portal_curl_post(api_key, endpoint, json) do
|
||||
tmp_file = "/tmp/un_ex_#{:rand.uniform(999999)}.json"
|
||||
File.write!(tmp_file, json)
|
||||
|
||||
{output, _exit} = System.cmd("curl", [
|
||||
{public_key, secret_key} = get_api_keys()
|
||||
headers = build_auth_headers(public_key, secret_key, "POST", endpoint, json)
|
||||
|
||||
args = [
|
||||
"-s", "-X", "POST",
|
||||
"#{@portal_base}#{endpoint}",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-H", "Authorization: Bearer #{api_key}",
|
||||
"-d", "@#{tmp_file}"
|
||||
], stderr_to_stdout: true)
|
||||
"-H", "Content-Type: application/json"
|
||||
] ++ headers ++ ["-d", "@#{tmp_file}"]
|
||||
|
||||
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
|
||||
|
||||
File.rm(tmp_file)
|
||||
output
|
||||
end
|
||||
|
||||
defp curl_get(api_key, endpoint) do
|
||||
{output, _exit} = System.cmd("curl", [
|
||||
{public_key, secret_key} = get_api_keys()
|
||||
headers = build_auth_headers(public_key, secret_key, "GET", endpoint, "")
|
||||
|
||||
args = [
|
||||
"-s",
|
||||
"https://api.unsandbox.com#{endpoint}",
|
||||
"-H", "Authorization: Bearer #{api_key}"
|
||||
], stderr_to_stdout: true)
|
||||
"https://api.unsandbox.com#{endpoint}"
|
||||
] ++ headers
|
||||
|
||||
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
defp curl_delete(api_key, endpoint) do
|
||||
{output, _exit} = System.cmd("curl", [
|
||||
{public_key, secret_key} = get_api_keys()
|
||||
headers = build_auth_headers(public_key, secret_key, "DELETE", endpoint, "")
|
||||
|
||||
args = [
|
||||
"-s", "-X", "DELETE",
|
||||
"https://api.unsandbox.com#{endpoint}",
|
||||
"-H", "Authorization: Bearer #{api_key}"
|
||||
], stderr_to_stdout: true)
|
||||
"https://api.unsandbox.com#{endpoint}"
|
||||
] ++ headers
|
||||
|
||||
{output, _exit} = System.cmd("curl", args, stderr_to_stdout: true)
|
||||
|
||||
output
|
||||
end
|
||||
|
|
|
|||
85
un.f90
85
un.f90
|
|
@ -87,8 +87,8 @@ contains
|
|||
|
||||
subroutine handle_execute(fname)
|
||||
character(len=*), intent(in) :: fname
|
||||
character(len=2048) :: full_cmd
|
||||
character(len=1024) :: env_opts, file_opts, net_opt
|
||||
character(len=4096) :: full_cmd
|
||||
character(len=1024) :: env_opts, file_opts, net_opt, public_key, secret_key
|
||||
integer :: i, arg_idx
|
||||
logical :: artifacts, has_env, has_files
|
||||
|
||||
|
|
@ -131,11 +131,23 @@ contains
|
|||
stop 1
|
||||
end if
|
||||
|
||||
! Get API key
|
||||
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat)
|
||||
if (stat /= 0 .or. len_trim(api_key) == 0) then
|
||||
write(0, '(A)') 'Error: UNSANDBOX_API_KEY environment variable not set'
|
||||
stop 1
|
||||
! Get API keys (try new format first, fall back to old)
|
||||
call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=stat)
|
||||
if (stat == 0 .and. len_trim(public_key) > 0) then
|
||||
call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=stat)
|
||||
if (stat /= 0 .or. len_trim(secret_key) == 0) then
|
||||
write(0, '(A)') 'Error: UNSANDBOX_SECRET_KEY not set'
|
||||
stop 1
|
||||
end if
|
||||
else
|
||||
! Fall back to old-style single key
|
||||
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat)
|
||||
if (stat /= 0 .or. len_trim(api_key) == 0) then
|
||||
write(0, '(A)') 'Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set'
|
||||
stop 1
|
||||
end if
|
||||
public_key = api_key
|
||||
secret_key = api_key
|
||||
end if
|
||||
|
||||
! Parse additional arguments (simple version - only support basic flags)
|
||||
|
|
@ -144,14 +156,17 @@ contains
|
|||
net_opt = ''
|
||||
artifacts = .false.
|
||||
|
||||
! Build curl command
|
||||
write(full_cmd, '(20A)') &
|
||||
! Build curl command with HMAC auth (use bash to compute signature)
|
||||
write(full_cmd, '(30A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'BODY=$(jq -Rs ''{language: "', trim(language), '", code: .}'' < "', trim(fname), '"); ', &
|
||||
'SIG=$(echo -n "$TS:POST:/execute:$BODY" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X POST https://api.unsandbox.com/execute ', &
|
||||
'-H "Content-Type: application/json" ', &
|
||||
'-H "Authorization: Bearer ', trim(api_key), '" ', &
|
||||
'--data-binary @- -o /tmp/unsandbox_resp.json ', &
|
||||
'< <(jq -Rs ''{language: "', trim(language), '", code: .}'' ', &
|
||||
'< "', trim(fname), '"); ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" ', &
|
||||
'--data-binary "$BODY" -o /tmp/unsandbox_resp.json; ', &
|
||||
'jq -r ".stdout // empty" /tmp/unsandbox_resp.json | ', &
|
||||
'sed "s/^/\x1b[34m/" | sed "s/$/\x1b[0m/"; ', &
|
||||
'jq -r ".stderr // empty" /tmp/unsandbox_resp.json | ', &
|
||||
|
|
@ -167,8 +182,9 @@ contains
|
|||
end subroutine handle_execute
|
||||
|
||||
subroutine handle_session()
|
||||
character(len=2048) :: full_cmd
|
||||
character(len=4096) :: full_cmd
|
||||
character(len=256) :: arg, session_id
|
||||
character(len=1024) :: public_key, secret_key
|
||||
integer :: i, stat
|
||||
logical :: list_mode, kill_mode
|
||||
|
||||
|
|
@ -189,27 +205,46 @@ contains
|
|||
end if
|
||||
end do
|
||||
|
||||
! Get API key
|
||||
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat)
|
||||
if (stat /= 0 .or. len_trim(api_key) == 0) then
|
||||
write(0, '(A)') 'Error: UNSANDBOX_API_KEY not set'
|
||||
stop 1
|
||||
! Get API keys (try new format first, fall back to old)
|
||||
call get_environment_variable('UNSANDBOX_PUBLIC_KEY', public_key, status=stat)
|
||||
if (stat == 0 .and. len_trim(public_key) > 0) then
|
||||
call get_environment_variable('UNSANDBOX_SECRET_KEY', secret_key, status=stat)
|
||||
if (stat /= 0 .or. len_trim(secret_key) == 0) then
|
||||
write(0, '(A)') 'Error: UNSANDBOX_SECRET_KEY not set'
|
||||
stop 1
|
||||
end if
|
||||
else
|
||||
call get_environment_variable('UNSANDBOX_API_KEY', api_key, status=stat)
|
||||
if (stat /= 0 .or. len_trim(api_key) == 0) then
|
||||
write(0, '(A)') 'Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set'
|
||||
stop 1
|
||||
end if
|
||||
public_key = api_key
|
||||
secret_key = api_key
|
||||
end if
|
||||
|
||||
if (list_mode) then
|
||||
! List sessions
|
||||
write(full_cmd, '(10A)') &
|
||||
! List sessions - GET request with empty body
|
||||
write(full_cmd, '(20A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:GET:/sessions:" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X GET https://api.unsandbox.com/sessions ', &
|
||||
'-H "Authorization: Bearer ', trim(api_key), '" | ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" | ', &
|
||||
'jq -r ''.sessions[] | "\(.id) \(.shell) \(.status) \(.created_at)"'' ', &
|
||||
'2>/dev/null || echo "No active sessions"'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else if (kill_mode .and. len_trim(session_id) > 0) then
|
||||
! Kill session
|
||||
write(full_cmd, '(10A)') &
|
||||
! Kill session - DELETE request with empty body
|
||||
write(full_cmd, '(20A)') &
|
||||
'TS=$(date +%s); ', &
|
||||
'SIG=$(echo -n "$TS:DELETE:/sessions/', trim(session_id), ':" | openssl dgst -sha256 -hmac "', trim(secret_key), '" | cut -d" " -f2); ', &
|
||||
'curl -s -X DELETE https://api.unsandbox.com/sessions/', &
|
||||
trim(session_id), ' ', &
|
||||
'-H "Authorization: Bearer ', trim(api_key), '" >/dev/null && ', &
|
||||
'-H "Authorization: Bearer ', trim(public_key), '" ', &
|
||||
'-H "X-Timestamp: $TS" ', &
|
||||
'-H "X-Signature: $SIG" >/dev/null && ', &
|
||||
'echo -e "\x1b[32mSession terminated: ', trim(session_id), '\x1b[0m"'
|
||||
call execute_command_line(trim(full_cmd), wait=.true.)
|
||||
else
|
||||
|
|
|
|||
211
un.forth
211
un.forth
|
|
@ -91,15 +91,30 @@
|
|||
find-ext ext-lang
|
||||
;
|
||||
|
||||
\ Get API key from environment
|
||||
: get-api-key ( -- addr len )
|
||||
s" UNSANDBOX_API_KEY" getenv
|
||||
\ Get API keys from environment (HMAC or legacy)
|
||||
: get-public-key ( -- addr len )
|
||||
s" UNSANDBOX_PUBLIC_KEY" getenv
|
||||
dup 0= if
|
||||
s" Error: UNSANDBOX_API_KEY not set" type cr
|
||||
2drop s" UNSANDBOX_API_KEY" getenv
|
||||
then
|
||||
dup 0= if
|
||||
s" Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set" type cr
|
||||
1 (bye)
|
||||
then
|
||||
;
|
||||
|
||||
: get-secret-key ( -- addr len )
|
||||
s" UNSANDBOX_SECRET_KEY" getenv
|
||||
dup 0= if
|
||||
2drop s" UNSANDBOX_API_KEY" getenv
|
||||
then
|
||||
;
|
||||
|
||||
\ Get API key (legacy compatibility)
|
||||
: get-api-key ( -- addr len )
|
||||
get-public-key
|
||||
;
|
||||
|
||||
\ Execute a file
|
||||
: execute-file ( addr len -- )
|
||||
\ Check file exists
|
||||
|
|
@ -125,8 +140,11 @@
|
|||
s" /tmp/unsandbox_script.sh" w/o create-file throw
|
||||
>r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" API_KEY='" r@ write-file throw
|
||||
get-api-key r@ write-file 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" LANG='" r@ write-file throw
|
||||
2swap 2drop \ drop language, keep filename on stack
|
||||
|
|
@ -135,7 +153,11 @@
|
|||
s" FILE='" r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\" --data-binary @- -o /tmp/unsandbox_resp.json < <(jq -Rs '{language: \"'$LANG'\", code: .}' < \"$FILE\"); jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'; jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2; rm -f /tmp/unsandbox_resp.json" r@ write-line throw
|
||||
s" BODY=$(jq -Rs '{language: \"'$LANG'\", code: .}' < \"$FILE\")" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/execute:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_resp.json; jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'; jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2; rm -f /tmp/unsandbox_resp.json" r@ write-line throw
|
||||
r> close-file throw
|
||||
|
||||
s" chmod +x /tmp/unsandbox_script.sh && /tmp/unsandbox_script.sh && rm -f /tmp/unsandbox_script.sh" system
|
||||
|
|
@ -147,9 +169,16 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/sessions -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' | jq -r '.sessions[] | \"\\(.id) \\(.shell) \\(.status) \\(.created_at)\"' 2>/dev/null || echo 'No active sessions'" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/sessions:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/sessions -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.sessions[] | \"\\(.id) \\(.shell) \\(.status) \\(.created_at)\"' 2>/dev/null || echo 'No active sessions'" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
|
@ -159,11 +188,19 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X DELETE https://api.unsandbox.com/sessions/" r@ write-file throw
|
||||
s" SESSION_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' >/dev/null && echo -e '\\x1b[32mSession terminated: " r@ write-file throw
|
||||
s" '" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:DELETE:/sessions/$SESSION_ID:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X DELETE https://api.unsandbox.com/sessions/$SESSION_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mSession terminated: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
|
|
@ -175,9 +212,16 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/services -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' | jq -r '.services[] | \"\\(.id) \\(.name) \\(.status)\"' 2>/dev/null || echo 'No services'" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/services:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/services -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.services[] | \"\\(.id) \\(.name) \\(.status)\"' 2>/dev/null || echo 'No services'" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
|
@ -187,11 +231,19 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/services/" r@ write-file throw
|
||||
s" SERVICE_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' | jq ." r@ write-line throw
|
||||
s" '" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/services/$SERVICE_ID:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/services/$SERVICE_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq ." r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
|
@ -201,11 +253,19 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/services/" r@ write-file throw
|
||||
s" SERVICE_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" /logs -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' | jq -r '.logs'" r@ write-line throw
|
||||
s" '" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:GET:/services/$SERVICE_ID/logs:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X GET https://api.unsandbox.com/services/$SERVICE_ID/logs -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" | jq -r '.logs'" r@ write-line throw
|
||||
r> close-file throw
|
||||
s" chmod +x /tmp/unsandbox_cmd.sh && /tmp/unsandbox_cmd.sh && rm -f /tmp/unsandbox_cmd.sh" system
|
||||
;
|
||||
|
|
@ -215,11 +275,19 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw
|
||||
s" SERVICE_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" /sleep -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' >/dev/null && echo -e '\\x1b[32mService sleeping: " r@ write-file throw
|
||||
s" '" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/services/$SERVICE_ID/sleep:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/services/$SERVICE_ID/sleep -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mService sleeping: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
|
|
@ -231,11 +299,19 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw
|
||||
s" SERVICE_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" /wake -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' >/dev/null && echo -e '\\x1b[32mService waking: " r@ write-file throw
|
||||
s" '" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/services/$SERVICE_ID/wake:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X POST https://api.unsandbox.com/services/$SERVICE_ID/wake -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mService waking: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
|
|
@ -247,11 +323,19 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" curl -s -X DELETE https://api.unsandbox.com/services/" r@ write-file throw
|
||||
s" SERVICE_ID='" r@ write-file throw
|
||||
2dup r@ write-file throw
|
||||
s" -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' >/dev/null && echo -e '\\x1b[32mService destroyed: " r@ write-file throw
|
||||
s" '" 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" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:DELETE:/services/$SERVICE_ID:\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" curl -s -X DELETE https://api.unsandbox.com/services/$SERVICE_ID -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" >/dev/null && echo -e '\\x1b[32mService destroyed: " r@ write-file throw
|
||||
r@ write-file throw
|
||||
s" \\x1b[0m'" r@ write-line throw
|
||||
r> close-file throw
|
||||
|
|
@ -263,14 +347,21 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" echo 'Fetching bootstrap script from " r@ write-file throw
|
||||
s" SERVICE_ID='" r@ write-file throw
|
||||
2over r@ write-file throw
|
||||
s" ...' >&2" r@ write-line throw
|
||||
s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/" r@ write-file throw
|
||||
2over r@ write-file throw
|
||||
s" /execute -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}')" r@ write-line throw
|
||||
s" '" 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" echo 'Fetching bootstrap script from $SERVICE_ID...' >&2" r@ write-line throw
|
||||
s" BODY='{\"command\":\"cat /tmp/bootstrap.sh\"}'" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/services/$SERVICE_ID/execute:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
s" RESP=$(curl -s -X POST https://api.unsandbox.com/services/$SERVICE_ID/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\")" r@ write-line throw
|
||||
s" STDOUT=$(echo \"$RESP\" | jq -r '.stdout // empty')" r@ write-line throw
|
||||
s" if [ -n \"$STDOUT\" ]; then" r@ write-line throw
|
||||
2dup 0 0 d= if
|
||||
|
|
@ -302,6 +393,12 @@
|
|||
\ For now, just create the curl command that will be constructed by bash
|
||||
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" 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
|
||||
|
|
@ -318,9 +415,10 @@
|
|||
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" curl -s -X POST https://api.unsandbox.com/services -H 'Content-Type: application/json' -H 'Authorization: Bearer " r@ write-file throw
|
||||
get-api-key r@ write-file throw
|
||||
s" ' -d \"$PAYLOAD\" | jq ." 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
|
||||
s" curl -s -X POST https://api.unsandbox.com/services -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$PAYLOAD\" | jq ." 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
|
||||
;
|
||||
|
|
@ -330,17 +428,24 @@
|
|||
get-api-key
|
||||
s" /tmp/unsandbox_key_cmd.sh" w/o create-file throw >r
|
||||
s" #!/bin/bash" r@ write-line throw
|
||||
s" API_KEY='" r@ write-file throw
|
||||
get-api-key r@ write-file 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" PORTAL_BASE='" r@ write-file throw
|
||||
portal-base r@ write-file throw
|
||||
s" '" r@ write-line throw
|
||||
s" BODY='{}'" r@ write-line throw
|
||||
s" TIMESTAMP=$(date +%s)" r@ write-line throw
|
||||
s" MESSAGE=\"$TIMESTAMP:POST:/keys/validate:$BODY\"" r@ write-line throw
|
||||
s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw
|
||||
|
||||
\ Check if extend flag is set
|
||||
0= if
|
||||
\ Normal validation
|
||||
s" curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\" -o /tmp/unsandbox_key_resp.json" r@ write-line throw
|
||||
s" curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_key_resp.json" r@ write-line throw
|
||||
s" STATUS=$?" r@ write-line throw
|
||||
s" if [ $STATUS -ne 0 ]; then" r@ write-line throw
|
||||
s" echo -e '\\x1b[31mInvalid\\x1b[0m'" r@ write-line throw
|
||||
|
|
@ -369,9 +474,9 @@
|
|||
s" rm -f /tmp/unsandbox_key_resp.json" r@ write-line throw
|
||||
else
|
||||
\ Extend mode
|
||||
s" RESP=$(curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $API_KEY\")" r@ write-line throw
|
||||
s" PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw
|
||||
s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$PUBLIC_KEY\" 2>/dev/null" r@ write-line throw
|
||||
s" RESP=$(curl -s -X POST $PORTAL_BASE/keys/validate -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\")" r@ write-line throw
|
||||
s" FETCHED_PUBLIC_KEY=$(echo \"$RESP\" | jq -r '.public_key // \"N/A\"')" r@ write-line throw
|
||||
s" xdg-open \"$PORTAL_BASE/keys/extend?pk=$FETCHED_PUBLIC_KEY\" 2>/dev/null" r@ write-line throw
|
||||
then
|
||||
|
||||
r> close-file throw
|
||||
|
|
|
|||
78
un.fs
78
un.fs
|
|
@ -44,6 +44,7 @@ open System
|
|||
open System.IO
|
||||
open System.Net
|
||||
open System.Text
|
||||
open System.Security.Cryptography
|
||||
|
||||
let apiBase = "https://api.unsandbox.com"
|
||||
let portalBase = "https://unsandbox.com"
|
||||
|
|
@ -101,12 +102,19 @@ type Args = {
|
|||
mutable KeyExtend: bool
|
||||
}
|
||||
|
||||
let getApiKey (argsKey: string option) =
|
||||
let key = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
|
||||
if String.IsNullOrEmpty(key) then
|
||||
eprintfn "%sError: UNSANDBOX_API_KEY not set%s" red reset
|
||||
exit 1
|
||||
key
|
||||
let getApiKeys (argsKey: string option) =
|
||||
let publicKey = Environment.GetEnvironmentVariable("UNSANDBOX_PUBLIC_KEY")
|
||||
let secretKey = Environment.GetEnvironmentVariable("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if String.IsNullOrEmpty(publicKey) || String.IsNullOrEmpty(secretKey) then
|
||||
let legacyKey = match argsKey with | Some k -> k | None -> Environment.GetEnvironmentVariable("UNSANDBOX_API_KEY")
|
||||
if String.IsNullOrEmpty(legacyKey) then
|
||||
eprintfn "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set%s" red reset
|
||||
exit 1
|
||||
(legacyKey, null)
|
||||
else
|
||||
(publicKey, secretKey)
|
||||
|
||||
let detectLanguage (filename: string) =
|
||||
let dotIndex = filename.LastIndexOf('.')
|
||||
|
|
@ -222,19 +230,35 @@ let parseJson (json: string) =
|
|||
|
||||
result |> Seq.map (fun (k, v) -> k, v) |> Map.ofSeq
|
||||
|
||||
let apiRequest (endpoint: string) (method: string) (data: (string * obj) list option) (apiKey: string) =
|
||||
let apiRequest (endpoint: string) (method: string) (data: (string * obj) list option) (publicKey: string) (secretKey: string) =
|
||||
ServicePointManager.SecurityProtocol <- SecurityProtocolType.Tls12 ||| SecurityProtocolType.Tls11 ||| SecurityProtocolType.Tls
|
||||
|
||||
let request = WebRequest.Create(apiBase + endpoint) :?> HttpWebRequest
|
||||
request.Method <- method
|
||||
request.ContentType <- "application/json"
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" apiKey)
|
||||
request.Timeout <- 300000
|
||||
|
||||
let body = match data with | Some d -> toJson (box d) | None -> ""
|
||||
|
||||
// Add HMAC authentication headers if secretKey is provided
|
||||
if not (String.IsNullOrEmpty(secretKey)) then
|
||||
let timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
let message = sprintf "%d:%s:%s:%s" timestamp method endpoint body
|
||||
|
||||
use hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey))
|
||||
let hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message))
|
||||
let signature = BitConverter.ToString(hash).Replace("-", "").ToLower()
|
||||
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
|
||||
request.Headers.Add("X-Timestamp", timestamp.ToString())
|
||||
request.Headers.Add("X-Signature", signature)
|
||||
else
|
||||
// Legacy API key authentication
|
||||
request.Headers.Add("Authorization", sprintf "Bearer %s" publicKey)
|
||||
|
||||
match data with
|
||||
| Some d ->
|
||||
let json = toJson (box d)
|
||||
let bytes = Encoding.UTF8.GetBytes(json)
|
||||
let bytes = Encoding.UTF8.GetBytes(body)
|
||||
request.ContentLength <- int64 bytes.Length
|
||||
use stream = request.GetRequestStream()
|
||||
stream.Write(bytes, 0, bytes.Length)
|
||||
|
|
@ -258,7 +282,7 @@ let apiRequest (endpoint: string) (method: string) (data: (string * obj) list op
|
|||
failwithf "HTTP error - %s" errorMsg
|
||||
|
||||
let cmdExecute (args: Args) =
|
||||
let apiKey = getApiKey args.ApiKey
|
||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||
let code = File.ReadAllText(args.SourceFile.Value)
|
||||
let language = detectLanguage args.SourceFile.Value
|
||||
|
||||
|
|
@ -286,7 +310,7 @@ let cmdExecute (args: Args) =
|
|||
if args.Vcpu > 0 then
|
||||
payload <- payload @ [("vcpu", box args.Vcpu)]
|
||||
|
||||
let result = apiRequest "/execute" "POST" (Some payload) apiKey
|
||||
let result = apiRequest "/execute" "POST" (Some payload) publicKey secretKey
|
||||
|
||||
match result.TryFind "stdout" with
|
||||
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
|
||||
|
|
@ -307,14 +331,14 @@ let cmdExecute (args: Args) =
|
|||
exit exitCode
|
||||
|
||||
let cmdSession (args: Args) =
|
||||
let apiKey = getApiKey args.ApiKey
|
||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||
|
||||
if args.SessionList then
|
||||
let result = apiRequest "/sessions" "GET" None apiKey
|
||||
let result = apiRequest "/sessions" "GET" None publicKey secretKey
|
||||
printfn "%-40s %-10s %-10s %s" "ID" "Shell" "Status" "Created"
|
||||
printfn "No sessions (list parsing not implemented)"
|
||||
elif args.SessionKill.IsSome then
|
||||
let result = apiRequest (sprintf "/sessions/%s" args.SessionKill.Value) "DELETE" None apiKey
|
||||
let result = apiRequest (sprintf "/sessions/%s" args.SessionKill.Value) "DELETE" None publicKey secretKey
|
||||
printfn "%sSession terminated: %s%s" green args.SessionKill.Value reset
|
||||
else
|
||||
let mutable payload = [("shell", box (match args.SessionShell with | Some s -> s | None -> "bash"))]
|
||||
|
|
@ -324,7 +348,7 @@ let cmdSession (args: Args) =
|
|||
payload <- payload @ [("vcpu", box args.Vcpu)]
|
||||
|
||||
printfn "%sCreating session...%s" yellow reset
|
||||
let result = apiRequest "/sessions" "POST" (Some payload) apiKey
|
||||
let result = apiRequest "/sessions" "POST" (Some payload) publicKey secretKey
|
||||
match result.TryFind "id" with
|
||||
| Some id -> printfn "%sSession created: %s%s" green (id.ToString()) reset
|
||||
| None -> printfn "%sSession created%s" green reset
|
||||
|
|
@ -412,37 +436,37 @@ let cmdKey (args: Args) =
|
|||
exit 1
|
||||
|
||||
let cmdService (args: Args) =
|
||||
let apiKey = getApiKey args.ApiKey
|
||||
let (publicKey, secretKey) = getApiKeys args.ApiKey
|
||||
|
||||
if args.ServiceList then
|
||||
let result = apiRequest "/services" "GET" None apiKey
|
||||
let result = apiRequest "/services" "GET" None publicKey secretKey
|
||||
printfn "%-20s %-15s %-10s %-15s %s" "ID" "Name" "Status" "Ports" "Domains"
|
||||
printfn "No services (list parsing not implemented)"
|
||||
elif args.ServiceInfo.IsSome then
|
||||
let result = apiRequest (sprintf "/services/%s" args.ServiceInfo.Value) "GET" None apiKey
|
||||
let result = apiRequest (sprintf "/services/%s" args.ServiceInfo.Value) "GET" None publicKey secretKey
|
||||
printfn "%s" (toJson (box result))
|
||||
elif args.ServiceLogs.IsSome then
|
||||
let result = apiRequest (sprintf "/services/%s/logs" args.ServiceLogs.Value) "GET" None apiKey
|
||||
let result = apiRequest (sprintf "/services/%s/logs" args.ServiceLogs.Value) "GET" None publicKey secretKey
|
||||
match result.TryFind "logs" with
|
||||
| Some logs -> printfn "%s" (logs.ToString())
|
||||
| None -> ()
|
||||
elif args.ServiceTail.IsSome then
|
||||
let result = apiRequest (sprintf "/services/%s/logs?lines=9000" args.ServiceTail.Value) "GET" None apiKey
|
||||
let result = apiRequest (sprintf "/services/%s/logs?lines=9000" args.ServiceTail.Value) "GET" None publicKey secretKey
|
||||
match result.TryFind "logs" with
|
||||
| Some logs -> printfn "%s" (logs.ToString())
|
||||
| None -> ()
|
||||
elif args.ServiceSleep.IsSome then
|
||||
let result = apiRequest (sprintf "/services/%s/sleep" args.ServiceSleep.Value) "POST" None apiKey
|
||||
let result = apiRequest (sprintf "/services/%s/sleep" args.ServiceSleep.Value) "POST" None publicKey secretKey
|
||||
printfn "%sService sleeping: %s%s" green args.ServiceSleep.Value reset
|
||||
elif args.ServiceWake.IsSome then
|
||||
let result = apiRequest (sprintf "/services/%s/wake" args.ServiceWake.Value) "POST" None apiKey
|
||||
let result = apiRequest (sprintf "/services/%s/wake" args.ServiceWake.Value) "POST" None publicKey secretKey
|
||||
printfn "%sService waking: %s%s" green args.ServiceWake.Value reset
|
||||
elif args.ServiceDestroy.IsSome then
|
||||
let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None apiKey
|
||||
let result = apiRequest (sprintf "/services/%s" args.ServiceDestroy.Value) "DELETE" None publicKey secretKey
|
||||
printfn "%sService destroyed: %s%s" green args.ServiceDestroy.Value reset
|
||||
elif args.ServiceExecute.IsSome then
|
||||
let payload = [("command", box args.ServiceCommand.Value)]
|
||||
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) apiKey
|
||||
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceExecute.Value) "POST" (Some payload) publicKey secretKey
|
||||
match result.TryFind "stdout" with
|
||||
| Some stdout when not (String.IsNullOrEmpty(stdout.ToString())) ->
|
||||
printf "%s%s%s" blue (stdout.ToString()) reset
|
||||
|
|
@ -454,7 +478,7 @@ let cmdService (args: Args) =
|
|||
elif args.ServiceDumpBootstrap.IsSome then
|
||||
eprintfn "Fetching bootstrap script from %s..." args.ServiceDumpBootstrap.Value
|
||||
let payload = [("command", box "cat /tmp/bootstrap.sh")]
|
||||
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) apiKey
|
||||
let result = apiRequest (sprintf "/services/%s/execute" args.ServiceDumpBootstrap.Value) "POST" (Some payload) publicKey secretKey
|
||||
|
||||
match result.TryFind "stdout" with
|
||||
| Some bootstrap when not (String.IsNullOrEmpty(bootstrap.ToString())) ->
|
||||
|
|
@ -485,7 +509,7 @@ let cmdService (args: Args) =
|
|||
if args.Vcpu > 0 then
|
||||
payload <- payload @ [("vcpu", box args.Vcpu)]
|
||||
|
||||
let result = apiRequest "/services" "POST" (Some payload) apiKey
|
||||
let result = apiRequest "/services" "POST" (Some payload) publicKey secretKey
|
||||
match result.TryFind "id" with
|
||||
| Some id -> printfn "%sService created: %s%s" green (id.ToString()) reset
|
||||
| None -> printfn "%sService created%s" green reset
|
||||
|
|
|
|||
109
un.go
109
un.go
|
|
@ -47,7 +47,10 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
|
@ -133,21 +136,38 @@ func detectLanguage(filename string) (string, error) {
|
|||
return "", fmt.Errorf("cannot detect language from extension")
|
||||
}
|
||||
|
||||
func getAPIKey(keyArg string) string {
|
||||
if keyArg != "" {
|
||||
return keyArg
|
||||
func getAPIKeys(keyArg string) (string, string) {
|
||||
publicKey := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
secretKey := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if publicKey == "" || secretKey == "" {
|
||||
fallbackKey := keyArg
|
||||
if fallbackKey == "" {
|
||||
fallbackKey = os.Getenv("UNSANDBOX_API_KEY")
|
||||
}
|
||||
if fallbackKey == "" {
|
||||
fmt.Fprintf(os.Stderr, "%sError: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)%s\n", Red, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Use fallback key as both public and secret for backwards compatibility
|
||||
return fallbackKey, fallbackKey
|
||||
}
|
||||
key := os.Getenv("UNSANDBOX_API_KEY")
|
||||
if key == "" {
|
||||
fmt.Fprintf(os.Stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", Red, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
return key
|
||||
|
||||
return publicKey, secretKey
|
||||
}
|
||||
|
||||
func apiRequest(endpoint, method string, data map[string]interface{}, apiKey string) map[string]interface{} {
|
||||
func computeHMAC(secretKey, timestamp, method, path, body string) string {
|
||||
message := fmt.Sprintf("%s:%s:%s:%s", timestamp, method, path, body)
|
||||
h := hmac.New(sha256.New, []byte(secretKey))
|
||||
h.Write([]byte(message))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func apiRequest(endpoint, method string, data map[string]interface{}, publicKey, secretKey string) map[string]interface{} {
|
||||
url := APIBase + endpoint
|
||||
var reqBody io.Reader
|
||||
bodyStr := ""
|
||||
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
|
|
@ -155,6 +175,7 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
|
|||
fmt.Fprintf(os.Stderr, "%sError marshaling JSON: %v%s\n", Red, err, Reset)
|
||||
os.Exit(1)
|
||||
}
|
||||
bodyStr = string(jsonData)
|
||||
reqBody = bytes.NewBuffer(jsonData)
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +185,13 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
// HMAC authentication
|
||||
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||||
signature := computeHMAC(secretKey, timestamp, method, endpoint, bodyStr)
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+publicKey)
|
||||
req.Header.Set("X-Timestamp", timestamp)
|
||||
req.Header.Set("X-Signature", signature)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
|
|
@ -195,7 +222,7 @@ func apiRequest(endpoint, method string, data map[string]interface{}, apiKey str
|
|||
return result
|
||||
}
|
||||
|
||||
func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts bool, outputDir, network string, vcpu int, apiKey string) {
|
||||
func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts bool, outputDir, network string, vcpu int, publicKey, secretKey string) {
|
||||
code, err := os.ReadFile(sourceFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%sError reading file: %v%s\n", Red, err, Reset)
|
||||
|
|
@ -254,7 +281,7 @@ func cmdExecute(sourceFile string, envs envVars, files inputFiles, artifacts boo
|
|||
payload["vcpu"] = vcpu
|
||||
}
|
||||
|
||||
result := apiRequest("/execute", "POST", payload, apiKey)
|
||||
result := apiRequest("/execute", "POST", payload, publicKey, secretKey)
|
||||
|
||||
// Print output
|
||||
if stdout, ok := result["stdout"].(string); ok && stdout != "" {
|
||||
|
|
@ -294,9 +321,9 @@ 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, apiKey string) {
|
||||
func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int, tmux, screen bool, publicKey, secretKey string) {
|
||||
if sessionList != "" {
|
||||
result := apiRequest("/sessions", "GET", nil, apiKey)
|
||||
result := apiRequest("/sessions", "GET", nil, publicKey, secretKey)
|
||||
sessions := result["sessions"].([]interface{})
|
||||
if len(sessions) == 0 {
|
||||
fmt.Println("No active sessions")
|
||||
|
|
@ -312,7 +339,7 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
|
|||
}
|
||||
|
||||
if sessionKill != "" {
|
||||
apiRequest("/sessions/"+sessionKill, "DELETE", nil, apiKey)
|
||||
apiRequest("/sessions/"+sessionKill, "DELETE", nil, publicKey, secretKey)
|
||||
fmt.Printf("%sSession terminated: %s%s\n", Green, sessionKill, Reset)
|
||||
return
|
||||
}
|
||||
|
|
@ -338,13 +365,13 @@ func cmdSession(sessionList, sessionKill, sessionShell, network string, vcpu int
|
|||
}
|
||||
|
||||
fmt.Printf("%sCreating session...%s\n", Yellow, Reset)
|
||||
result := apiRequest("/sessions", "POST", payload, apiKey)
|
||||
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, apiKey string) {
|
||||
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) {
|
||||
if serviceList != "" {
|
||||
result := apiRequest("/services", "GET", nil, apiKey)
|
||||
result := apiRequest("/services", "GET", nil, publicKey, secretKey)
|
||||
services := result["services"].([]interface{})
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No services")
|
||||
|
|
@ -376,45 +403,45 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
|
|||
}
|
||||
|
||||
if serviceInfo != "" {
|
||||
result := apiRequest("/services/"+serviceInfo, "GET", nil, apiKey)
|
||||
result := apiRequest("/services/"+serviceInfo, "GET", nil, publicKey, secretKey)
|
||||
jsonData, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Println(string(jsonData))
|
||||
return
|
||||
}
|
||||
|
||||
if serviceLogs != "" {
|
||||
result := apiRequest("/services/"+serviceLogs+"/logs", "GET", nil, apiKey)
|
||||
result := apiRequest("/services/"+serviceLogs+"/logs", "GET", nil, publicKey, secretKey)
|
||||
fmt.Print(result["logs"])
|
||||
return
|
||||
}
|
||||
|
||||
if serviceTail != "" {
|
||||
result := apiRequest("/services/"+serviceTail+"/logs?lines=9000", "GET", nil, apiKey)
|
||||
result := apiRequest("/services/"+serviceTail+"/logs?lines=9000", "GET", nil, publicKey, secretKey)
|
||||
fmt.Print(result["logs"])
|
||||
return
|
||||
}
|
||||
|
||||
if serviceSleep != "" {
|
||||
apiRequest("/services/"+serviceSleep+"/sleep", "POST", nil, apiKey)
|
||||
apiRequest("/services/"+serviceSleep+"/sleep", "POST", nil, publicKey, secretKey)
|
||||
fmt.Printf("%sService sleeping: %s%s\n", Green, serviceSleep, Reset)
|
||||
return
|
||||
}
|
||||
|
||||
if serviceWake != "" {
|
||||
apiRequest("/services/"+serviceWake+"/wake", "POST", nil, apiKey)
|
||||
apiRequest("/services/"+serviceWake+"/wake", "POST", nil, publicKey, secretKey)
|
||||
fmt.Printf("%sService waking: %s%s\n", Green, serviceWake, Reset)
|
||||
return
|
||||
}
|
||||
|
||||
if serviceDestroy != "" {
|
||||
apiRequest("/services/"+serviceDestroy, "DELETE", nil, apiKey)
|
||||
apiRequest("/services/"+serviceDestroy, "DELETE", nil, publicKey, secretKey)
|
||||
fmt.Printf("%sService destroyed: %s%s\n", Green, serviceDestroy, Reset)
|
||||
return
|
||||
}
|
||||
|
||||
if serviceExecute != "" {
|
||||
payload := map[string]interface{}{"command": serviceCommand}
|
||||
result := apiRequest("/services/"+serviceExecute+"/execute", "POST", payload, apiKey)
|
||||
result := apiRequest("/services/"+serviceExecute+"/execute", "POST", payload, publicKey, secretKey)
|
||||
if stdout, ok := result["stdout"].(string); ok {
|
||||
fmt.Printf("%s%s%s", Blue, stdout, Reset)
|
||||
}
|
||||
|
|
@ -427,7 +454,7 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
|
|||
if serviceDumpBootstrap != "" {
|
||||
fmt.Fprintf(os.Stderr, "Fetching bootstrap script from %s...\n", serviceDumpBootstrap)
|
||||
payload := map[string]interface{}{"command": "cat /tmp/bootstrap.sh"}
|
||||
result := apiRequest("/services/"+serviceDumpBootstrap+"/execute", "POST", payload, apiKey)
|
||||
result := apiRequest("/services/"+serviceDumpBootstrap+"/execute", "POST", payload, publicKey, secretKey)
|
||||
|
||||
if bootstrap, ok := result["stdout"].(string); ok && bootstrap != "" {
|
||||
if serviceDumpFile != "" {
|
||||
|
|
@ -482,7 +509,7 @@ func cmdService(serviceName, servicePorts, serviceDomains, serviceType, serviceB
|
|||
payload["vcpu"] = vcpu
|
||||
}
|
||||
|
||||
result := apiRequest("/services", "POST", payload, apiKey)
|
||||
result := apiRequest("/services", "POST", payload, publicKey, secretKey)
|
||||
fmt.Printf("%sService created: %s%s\n", Green, result["id"], Reset)
|
||||
fmt.Printf("Name: %s\n", result["name"])
|
||||
if url, ok := result["url"]; ok {
|
||||
|
|
@ -524,7 +551,7 @@ func formatDuration(d time.Duration) string {
|
|||
}
|
||||
}
|
||||
|
||||
func validateKey(apiKey string, extend bool) {
|
||||
func validateKey(publicKey, secretKey string, extend bool) {
|
||||
url := PortalBase + "/keys/validate"
|
||||
reqBody := bytes.NewBuffer(nil)
|
||||
|
||||
|
|
@ -534,7 +561,13 @@ func validateKey(apiKey string, extend bool) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
// HMAC authentication
|
||||
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||||
signature := computeHMAC(secretKey, timestamp, "POST", "/keys/validate", "")
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+publicKey)
|
||||
req.Header.Set("X-Timestamp", timestamp)
|
||||
req.Header.Set("X-Signature", signature)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
|
|
@ -702,7 +735,7 @@ func main() {
|
|||
switch os.Args[1] {
|
||||
case "session":
|
||||
sessionCmd.Parse(os.Args[2:])
|
||||
key := getAPIKey(*sessionKey)
|
||||
publicKey, secretKey := getAPIKeys(*sessionKey)
|
||||
net := *sessionNetwork
|
||||
if net == "" {
|
||||
net = *network
|
||||
|
|
@ -711,12 +744,12 @@ func main() {
|
|||
if vc == 0 {
|
||||
vc = *vcpu
|
||||
}
|
||||
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, key)
|
||||
cmdSession(*sessionList, *sessionKill, *sessionShell, net, vc, *sessionTmux, *sessionScreen, publicKey, secretKey)
|
||||
return
|
||||
|
||||
case "service":
|
||||
serviceCmd.Parse(os.Args[2:])
|
||||
key := getAPIKey(*serviceKey)
|
||||
publicKey, secretKey := getAPIKeys(*serviceKey)
|
||||
net := *serviceNetwork
|
||||
if net == "" {
|
||||
net = *network
|
||||
|
|
@ -725,13 +758,13 @@ 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, key)
|
||||
cmdService(*serviceName, *servicePorts, *serviceDomains, *serviceType, *serviceBootstrap, *serviceList, *serviceInfo, *serviceLogs, *serviceTail, *serviceSleep, *serviceWake, *serviceDestroy, *serviceExecute, *serviceCommand, *serviceDumpBootstrap, *serviceDumpFile, net, vc, publicKey, secretKey)
|
||||
return
|
||||
|
||||
case "key":
|
||||
keyCmd.Parse(os.Args[2:])
|
||||
key := getAPIKey(*keyKey)
|
||||
validateKey(key, *keyExtend)
|
||||
publicKey, secretKey := getAPIKeys(*keyKey)
|
||||
validateKey(publicKey, secretKey, *keyExtend)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -746,6 +779,6 @@ func main() {
|
|||
}
|
||||
|
||||
sourceFile := flag.Arg(0)
|
||||
key := getAPIKey(*apiKey)
|
||||
cmdExecute(sourceFile, envs, files, *artifacts, *outputDir, *network, *vcpu, key)
|
||||
publicKey, secretKey := getAPIKeys(*apiKey)
|
||||
cmdExecute(sourceFile, envs, files, *artifacts, *outputDir, *network, *vcpu, publicKey, secretKey)
|
||||
}
|
||||
|
|
|
|||
104
un.groovy
104
un.groovy
|
|
@ -92,13 +92,24 @@ class Args {
|
|||
Boolean keyExtend = false
|
||||
}
|
||||
|
||||
def getApiKey(argsKey) {
|
||||
def key = argsKey ?: System.getenv('UNSANDBOX_API_KEY')
|
||||
if (!key) {
|
||||
System.err.println("${RED}Error: UNSANDBOX_API_KEY not set${RESET}")
|
||||
System.exit(1)
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
def getApiKeys(argsKey) {
|
||||
def publicKey = System.getenv('UNSANDBOX_PUBLIC_KEY')
|
||||
def secretKey = System.getenv('UNSANDBOX_SECRET_KEY')
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if (!publicKey || !secretKey) {
|
||||
def legacyKey = argsKey ?: System.getenv('UNSANDBOX_API_KEY')
|
||||
if (!legacyKey) {
|
||||
System.err.println("${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}")
|
||||
System.exit(1)
|
||||
}
|
||||
return [legacyKey, null]
|
||||
}
|
||||
return key
|
||||
|
||||
return [publicKey, secretKey]
|
||||
}
|
||||
|
||||
def detectLanguage(filename) {
|
||||
|
|
@ -116,16 +127,33 @@ def detectLanguage(filename) {
|
|||
return language
|
||||
}
|
||||
|
||||
def apiRequest(endpoint, method, data, apiKey) {
|
||||
def apiRequest(endpoint, method, data, publicKey, secretKey) {
|
||||
def tempFile = File.createTempFile('un_request_', '.json')
|
||||
try {
|
||||
def body = data ?: ""
|
||||
if (data) {
|
||||
tempFile.text = data
|
||||
}
|
||||
|
||||
def curlCmd = ['curl', '-s', '-X', method, "${API_BASE}${endpoint}",
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-H', "Authorization: Bearer ${apiKey}"]
|
||||
'-H', 'Content-Type: application/json']
|
||||
|
||||
// Add HMAC authentication headers if secretKey is provided
|
||||
if (secretKey) {
|
||||
def timestamp = (System.currentTimeMillis() / 1000) as long
|
||||
def message = "${timestamp}:${method}:${endpoint}:${body}"
|
||||
|
||||
def mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
|
||||
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
|
||||
|
||||
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
|
||||
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
|
||||
curlCmd += ['-H', "X-Signature: ${signature}"]
|
||||
} else {
|
||||
// Legacy API key authentication
|
||||
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
|
||||
}
|
||||
|
||||
if (data) {
|
||||
curlCmd += ['-d', "@${tempFile.absolutePath}"]
|
||||
|
|
@ -147,7 +175,7 @@ def apiRequest(endpoint, method, data, apiKey) {
|
|||
}
|
||||
|
||||
def cmdExecute(args) {
|
||||
def apiKey = getApiKey(args.apiKey)
|
||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
def file = new File(args.sourceFile)
|
||||
if (!file.exists()) {
|
||||
System.err.println("${RED}Error: File not found: ${args.sourceFile}${RESET}")
|
||||
|
|
@ -203,7 +231,7 @@ def cmdExecute(args) {
|
|||
|
||||
json += '}'
|
||||
|
||||
def output = apiRequest('/execute', 'POST', json, apiKey)
|
||||
def output = apiRequest('/execute', 'POST', json, publicKey, secretKey)
|
||||
|
||||
def stdoutMatch = output =~ /"stdout":"((?:[^"\\]|\\.)*)"/
|
||||
def stderrMatch = output =~ /"stderr":"((?:[^"\\]|\\.)*)"/
|
||||
|
|
@ -245,17 +273,17 @@ def cmdExecute(args) {
|
|||
}
|
||||
|
||||
def cmdSession(args) {
|
||||
def apiKey = getApiKey(args.apiKey)
|
||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
if (args.sessionList) {
|
||||
def output = apiRequest('/sessions', 'GET', null, apiKey)
|
||||
def output = apiRequest('/sessions', 'GET', null, publicKey, secretKey)
|
||||
println("%-40s %-10s %-10s %s".format("ID", "Shell", "Status", "Created"))
|
||||
println("No sessions (list parsing not implemented)")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.sessionKill) {
|
||||
apiRequest("/sessions/${args.sessionKill}", 'DELETE', null, apiKey)
|
||||
apiRequest("/sessions/${args.sessionKill}", 'DELETE', null, publicKey, secretKey)
|
||||
println("${GREEN}Session terminated: ${args.sessionKill}${RESET}")
|
||||
return
|
||||
}
|
||||
|
|
@ -270,7 +298,7 @@ def cmdSession(args) {
|
|||
json += '}'
|
||||
|
||||
println("${YELLOW}Creating session...${RESET}")
|
||||
def output = apiRequest('/sessions', 'POST', json, apiKey)
|
||||
def output = apiRequest('/sessions', 'POST', json, publicKey, secretKey)
|
||||
def idMatch = output =~ /"id":"([^"]+)"/
|
||||
if (idMatch.find()) {
|
||||
println("${GREEN}Session created: ${idMatch.group(1)}${RESET}")
|
||||
|
|
@ -296,12 +324,28 @@ def openBrowser(url) {
|
|||
}
|
||||
|
||||
def cmdKey(args) {
|
||||
def apiKey = getApiKey(args.apiKey)
|
||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
def curlCmd = ['curl', '-s', '-X', 'POST', "${PORTAL_BASE}/keys/validate",
|
||||
'-H', 'Content-Type: application/json',
|
||||
'-H', "Authorization: Bearer ${apiKey}",
|
||||
'-d', '{}']
|
||||
'-H', 'Content-Type: application/json']
|
||||
|
||||
// Add HMAC authentication headers if secretKey is provided
|
||||
if (secretKey) {
|
||||
def timestamp = (System.currentTimeMillis() / 1000) as long
|
||||
def message = "${timestamp}:POST:/keys/validate:{}"
|
||||
|
||||
def mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256"))
|
||||
def signature = mac.doFinal(message.getBytes("UTF-8")).encodeHex().toString()
|
||||
|
||||
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
|
||||
curlCmd += ['-H', "X-Timestamp: ${timestamp}"]
|
||||
curlCmd += ['-H', "X-Signature: ${signature}"]
|
||||
} else {
|
||||
curlCmd += ['-H', "Authorization: Bearer ${publicKey}"]
|
||||
}
|
||||
|
||||
curlCmd += ['-d', '{}']
|
||||
|
||||
def proc = curlCmd.execute()
|
||||
def output = proc.text
|
||||
|
|
@ -361,23 +405,23 @@ def cmdKey(args) {
|
|||
}
|
||||
|
||||
def cmdService(args) {
|
||||
def apiKey = getApiKey(args.apiKey)
|
||||
def (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
if (args.serviceList) {
|
||||
def output = apiRequest('/services', 'GET', null, apiKey)
|
||||
def output = apiRequest('/services', 'GET', null, publicKey, secretKey)
|
||||
println("%-20s %-15s %-10s %-15s %s".format("ID", "Name", "Status", "Ports", "Domains"))
|
||||
println("No services (list parsing not implemented)")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceInfo) {
|
||||
def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, apiKey)
|
||||
def output = apiRequest("/services/${args.serviceInfo}", 'GET', null, publicKey, secretKey)
|
||||
println(output)
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceLogs) {
|
||||
def output = apiRequest("/services/${args.serviceLogs}/logs", 'GET', null, apiKey)
|
||||
def output = apiRequest("/services/${args.serviceLogs}/logs", 'GET', null, publicKey, secretKey)
|
||||
def logsMatch = output =~ /"logs":"((?:[^"\\]|\\.)*)"/
|
||||
if (logsMatch.find()) {
|
||||
println(logsMatch.group(1).replace('\\n', '\n'))
|
||||
|
|
@ -386,7 +430,7 @@ def cmdService(args) {
|
|||
}
|
||||
|
||||
if (args.serviceTail) {
|
||||
def output = apiRequest("/services/${args.serviceTail}/logs?lines=9000", 'GET', null, apiKey)
|
||||
def output = apiRequest("/services/${args.serviceTail}/logs?lines=9000", 'GET', null, publicKey, secretKey)
|
||||
def logsMatch = output =~ /"logs":"((?:[^"\\]|\\.)*)"/
|
||||
if (logsMatch.find()) {
|
||||
println(logsMatch.group(1).replace('\\n', '\n'))
|
||||
|
|
@ -395,26 +439,26 @@ def cmdService(args) {
|
|||
}
|
||||
|
||||
if (args.serviceSleep) {
|
||||
apiRequest("/services/${args.serviceSleep}/sleep", 'POST', null, apiKey)
|
||||
apiRequest("/services/${args.serviceSleep}/sleep", 'POST', null, publicKey, secretKey)
|
||||
println("${GREEN}Service sleeping: ${args.serviceSleep}${RESET}")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceWake) {
|
||||
apiRequest("/services/${args.serviceWake}/wake", 'POST', null, apiKey)
|
||||
apiRequest("/services/${args.serviceWake}/wake", 'POST', null, publicKey, secretKey)
|
||||
println("${GREEN}Service waking: ${args.serviceWake}${RESET}")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceDestroy) {
|
||||
apiRequest("/services/${args.serviceDestroy}", 'DELETE', null, apiKey)
|
||||
apiRequest("/services/${args.serviceDestroy}", 'DELETE', null, publicKey, secretKey)
|
||||
println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceExecute) {
|
||||
def json = """{"command":"${args.serviceCommand}"}"""
|
||||
def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, apiKey)
|
||||
def output = apiRequest("/services/${args.serviceExecute}/execute", 'POST', json, publicKey, secretKey)
|
||||
def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/
|
||||
def stderrMatch = output =~ /"stderr":"((?:[^"\\\\]|\\\\.)*)"/
|
||||
|
||||
|
|
@ -441,7 +485,7 @@ def cmdService(args) {
|
|||
if (args.serviceDumpBootstrap) {
|
||||
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
|
||||
def json = """{"command":"cat /tmp/bootstrap.sh"}"""
|
||||
def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST', json, apiKey)
|
||||
def output = apiRequest("/services/${args.serviceDumpBootstrap}/execute", 'POST', json, publicKey, secretKey)
|
||||
|
||||
def stdoutMatch = output =~ /"stdout":"((?:[^"\\\\]|\\\\.)*)"/
|
||||
if (stdoutMatch.find()) {
|
||||
|
|
@ -491,7 +535,7 @@ def cmdService(args) {
|
|||
}
|
||||
json += '}'
|
||||
|
||||
def output = apiRequest('/services', 'POST', json, apiKey)
|
||||
def output = apiRequest('/services', 'POST', json, publicKey, secretKey)
|
||||
def idMatch = output =~ /"id":"([^"]+)"/
|
||||
if (idMatch.find()) {
|
||||
println("${GREEN}Service created: ${idMatch.group(1)}${RESET}")
|
||||
|
|
|
|||
101
un.hs
101
un.hs
|
|
@ -62,11 +62,15 @@ import System.Process (readProcessWithExitCode)
|
|||
import System.IO (hPutStrLn, stderr)
|
||||
import System.Directory (createDirectoryIfMissing, setPermissions, getPermissions, setOwnerExecutable)
|
||||
import Data.List (isPrefixOf, intercalate)
|
||||
import Data.Char (isDigit)
|
||||
import Data.Char (isDigit, ord)
|
||||
import Text.Printf (printf)
|
||||
import Control.Monad (when, unless, forM_)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BSC
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Crypto.Hash.SHA256 (hmac)
|
||||
import Numeric (showHex)
|
||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||
|
||||
-- API constants
|
||||
apiBase :: String
|
||||
|
|
@ -382,39 +386,77 @@ serviceCommand opts = do
|
|||
-- HTTP helpers using curl
|
||||
curlPost :: String -> String -> String -> IO (ExitCode, String, String)
|
||||
curlPost apiKey url body = do
|
||||
(publicKey, secretKey) <- getApiKeys
|
||||
-- Extract path from URL
|
||||
let path = drop (length "https://api.unsandbox.com") url
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "POST" path body
|
||||
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
|
||||
[ "-s", "-X", "POST"
|
||||
, url
|
||||
, "-H", "Content-Type: application/json"
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
, "-d", body
|
||||
] ""
|
||||
([ "-s", "-X", "POST"
|
||||
, url
|
||||
, "-H", "Content-Type: application/json"
|
||||
] ++ authHeaders ++ ["-d", body]) ""
|
||||
return (exitCode, stdout, stderr)
|
||||
|
||||
curlGet :: String -> String -> IO (ExitCode, String, String)
|
||||
curlGet apiKey url =
|
||||
curlGet apiKey url = do
|
||||
(publicKey, secretKey) <- getApiKeys
|
||||
let path = drop (length "https://api.unsandbox.com") url
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "GET" path ""
|
||||
readProcessWithExitCode "curl"
|
||||
[ "-s", url
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
] ""
|
||||
([ "-s", url ] ++ authHeaders) ""
|
||||
|
||||
curlDelete :: String -> String -> IO (ExitCode, String, String)
|
||||
curlDelete apiKey url =
|
||||
curlDelete apiKey url = do
|
||||
(publicKey, secretKey) <- getApiKeys
|
||||
let path = drop (length "https://api.unsandbox.com") url
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "DELETE" path ""
|
||||
readProcessWithExitCode "curl"
|
||||
[ "-s", "-X", "DELETE"
|
||||
, url
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
] ""
|
||||
([ "-s", "-X", "DELETE", url ] ++ authHeaders) ""
|
||||
|
||||
-- Get API keys from environment
|
||||
getApiKeys :: IO (String, Maybe String)
|
||||
getApiKeys = do
|
||||
publicKey <- lookupEnv "UNSANDBOX_PUBLIC_KEY"
|
||||
secretKey <- lookupEnv "UNSANDBOX_SECRET_KEY"
|
||||
apiKey <- lookupEnv "UNSANDBOX_API_KEY"
|
||||
case (publicKey, secretKey, apiKey) of
|
||||
(Just pk, Just sk, _) -> return (pk, Just sk)
|
||||
(_, _, Just ak) -> return (ak, Nothing)
|
||||
_ -> do
|
||||
hPutStrLn stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)"
|
||||
exitFailure
|
||||
|
||||
-- Get API key from environment
|
||||
getApiKey :: IO String
|
||||
getApiKey = do
|
||||
maybeKey <- lookupEnv "UNSANDBOX_API_KEY"
|
||||
case maybeKey of
|
||||
Nothing -> do
|
||||
hPutStrLn stderr "Error: UNSANDBOX_API_KEY not set"
|
||||
exitFailure
|
||||
Just key -> return key
|
||||
(publicKey, _) <- getApiKeys
|
||||
return publicKey
|
||||
|
||||
-- HMAC-SHA256
|
||||
hmacSha256 :: String -> String -> String
|
||||
hmacSha256 secret message =
|
||||
let secretBS = BSC.pack secret
|
||||
messageBS = BSC.pack message
|
||||
mac = hmac secretBS messageBS
|
||||
in concatMap (printf "%02x") (BS.unpack mac)
|
||||
|
||||
makeSignature :: String -> String -> String -> String -> String -> String
|
||||
makeSignature secretKey timestamp method path body =
|
||||
let message = timestamp ++ ":" ++ method ++ ":" ++ path ++ ":" ++ body
|
||||
in hmacSha256 secretKey message
|
||||
|
||||
buildAuthHeaders :: String -> Maybe String -> String -> String -> String -> IO [String]
|
||||
buildAuthHeaders publicKey maybeSecretKey method path body =
|
||||
case maybeSecretKey of
|
||||
Just secretKey -> do
|
||||
now <- getPOSIXTime
|
||||
let timestamp = show (floor now :: Integer)
|
||||
let signature = makeSignature secretKey timestamp method path body
|
||||
return [ "-H", "Authorization: Bearer " ++ publicKey
|
||||
, "-H", "X-Timestamp: " ++ timestamp
|
||||
, "-H", "X-Signature: " ++ signature
|
||||
]
|
||||
Nothing ->
|
||||
return ["-H", "Authorization: Bearer " ++ publicKey]
|
||||
|
||||
-- Parse exit code from JSON response
|
||||
parseExitCode :: String -> Int
|
||||
|
|
@ -565,11 +607,12 @@ extendKey apiKey = do
|
|||
-- HTTP helper for portal API
|
||||
curlPostPortal :: String -> String -> String -> IO (ExitCode, String, String)
|
||||
curlPostPortal apiKey url body = do
|
||||
(publicKey, secretKey) <- getApiKeys
|
||||
let path = drop (length portalBase) url
|
||||
authHeaders <- buildAuthHeaders publicKey secretKey "POST" path body
|
||||
(exitCode, stdout, stderr) <- readProcessWithExitCode "curl"
|
||||
[ "-s", "-X", "POST"
|
||||
, url
|
||||
, "-H", "Content-Type: application/json"
|
||||
, "-H", "Authorization: Bearer " ++ apiKey
|
||||
, "-d", body
|
||||
] ""
|
||||
([ "-s", "-X", "POST"
|
||||
, url
|
||||
, "-H", "Content-Type: application/json"
|
||||
] ++ authHeaders ++ ["-d", body]) ""
|
||||
return (exitCode, stdout, stderr)
|
||||
|
|
|
|||
83
un.jl
83
un.jl
|
|
@ -42,6 +42,7 @@ using JSON
|
|||
using Base64
|
||||
using ArgParse
|
||||
using Printf
|
||||
using SHA
|
||||
|
||||
# Extension to language mapping
|
||||
const EXT_MAP = Dict(
|
||||
|
|
@ -75,19 +76,54 @@ function detect_language(filename::String)::String
|
|||
return get(EXT_MAP, ext, "unknown")
|
||||
end
|
||||
|
||||
function get_api_key(args_key=nothing)::String
|
||||
key = something(args_key, get(ENV, "UNSANDBOX_API_KEY", ""))
|
||||
if isempty(key)
|
||||
println(stderr, "$(RED)Error: UNSANDBOX_API_KEY not set$(RESET)")
|
||||
function get_api_keys(args_key=nothing)::Tuple{String,String}
|
||||
# Try new-style keys first
|
||||
public_key = something(args_key, get(ENV, "UNSANDBOX_PUBLIC_KEY", ""))
|
||||
secret_key = get(ENV, "UNSANDBOX_SECRET_KEY", "")
|
||||
|
||||
# Fall back to old-style single key for backwards compatibility
|
||||
if isempty(public_key)
|
||||
old_key = get(ENV, "UNSANDBOX_API_KEY", "")
|
||||
if isempty(old_key)
|
||||
println(stderr, "$(RED)Error: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
# Old-style: use same key for both public and secret
|
||||
return (old_key, old_key)
|
||||
end
|
||||
|
||||
if isempty(secret_key)
|
||||
println(stderr, "$(RED)Error: UNSANDBOX_SECRET_KEY not set$(RESET)")
|
||||
exit(1)
|
||||
end
|
||||
return key
|
||||
|
||||
return (public_key, secret_key)
|
||||
end
|
||||
|
||||
function api_request(endpoint::String, api_key::String; method="GET", data=nothing)
|
||||
function hmac_sha256_hex(key::String, message::String)::String
|
||||
h = hmac_sha256(Vector{UInt8}(key), Vector{UInt8}(message))
|
||||
return bytes2hex(h)
|
||||
end
|
||||
|
||||
function compute_signature(secret_key::String, timestamp::Int64, method::String, path::String, body::String)::String
|
||||
message = "$(timestamp):$(method):$(path):$(body)"
|
||||
return hmac_sha256_hex(secret_key, message)
|
||||
end
|
||||
|
||||
function api_request(endpoint::String, public_key::String, secret_key::String; method="GET", data=nothing)
|
||||
url = API_BASE * endpoint
|
||||
|
||||
# Prepare body
|
||||
body = data !== nothing ? JSON.json(data) : ""
|
||||
|
||||
# Generate timestamp and signature
|
||||
timestamp = Int64(floor(time()))
|
||||
signature = compute_signature(secret_key, timestamp, method, endpoint, body)
|
||||
|
||||
headers = [
|
||||
"Authorization" => "Bearer $api_key",
|
||||
"Authorization" => "Bearer $public_key",
|
||||
"X-Timestamp" => string(timestamp),
|
||||
"X-Signature" => signature,
|
||||
"Content-Type" => "application/json"
|
||||
]
|
||||
|
||||
|
|
@ -95,7 +131,6 @@ function api_request(endpoint::String, api_key::String; method="GET", data=nothi
|
|||
if method == "GET"
|
||||
response = HTTP.get(url, headers, readtimeout=300)
|
||||
elseif method == "POST"
|
||||
body = data !== nothing ? JSON.json(data) : ""
|
||||
response = HTTP.post(url, headers, body, readtimeout=300)
|
||||
elseif method == "DELETE"
|
||||
response = HTTP.delete(url, headers, readtimeout=300)
|
||||
|
|
@ -115,7 +150,7 @@ function api_request(endpoint::String, api_key::String; method="GET", data=nothi
|
|||
end
|
||||
|
||||
function cmd_execute(args)
|
||||
api_key = get_api_key(args["api-key"])
|
||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
||||
|
||||
filename = args["source_file"]
|
||||
if !isfile(filename)
|
||||
|
|
@ -176,7 +211,7 @@ function cmd_execute(args)
|
|||
end
|
||||
|
||||
# Execute
|
||||
result = api_request("/execute", api_key, method="POST", data=payload)
|
||||
result = api_request("/execute", public_key, secret_key, method="POST", data=payload)
|
||||
|
||||
# Print output
|
||||
if haskey(result, "stdout") && !isempty(result["stdout"])
|
||||
|
|
@ -205,10 +240,10 @@ function cmd_execute(args)
|
|||
end
|
||||
|
||||
function cmd_session(args)
|
||||
api_key = get_api_key(args["api-key"])
|
||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
||||
|
||||
if args["list"]
|
||||
result = api_request("/sessions", api_key)
|
||||
result = api_request("/sessions", public_key, secret_key)
|
||||
sessions = get(result, "sessions", [])
|
||||
if isempty(sessions)
|
||||
println("No active sessions")
|
||||
|
|
@ -226,7 +261,7 @@ function cmd_session(args)
|
|||
end
|
||||
|
||||
if args["kill"] !== nothing
|
||||
api_request("/sessions/$(args["kill"])", api_key, method="DELETE")
|
||||
api_request("/sessions/$(args["kill"])", public_key, secret_key, method="DELETE")
|
||||
println("$(GREEN)Session terminated: $(args["kill"])$(RESET)")
|
||||
return
|
||||
end
|
||||
|
|
@ -236,10 +271,10 @@ function cmd_session(args)
|
|||
end
|
||||
|
||||
function cmd_service(args)
|
||||
api_key = get_api_key(args["api-key"])
|
||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
||||
|
||||
if args["list"]
|
||||
result = api_request("/services", api_key)
|
||||
result = api_request("/services", public_key, secret_key)
|
||||
services = get(result, "services", [])
|
||||
if isempty(services)
|
||||
println("No services")
|
||||
|
|
@ -259,31 +294,31 @@ function cmd_service(args)
|
|||
end
|
||||
|
||||
if args["info"] !== nothing
|
||||
result = api_request("/services/$(args["info"])", api_key)
|
||||
result = api_request("/services/$(args["info"])", public_key, secret_key)
|
||||
println(JSON.json(result, 2))
|
||||
return
|
||||
end
|
||||
|
||||
if args["logs"] !== nothing
|
||||
result = api_request("/services/$(args["logs"])/logs", api_key)
|
||||
result = api_request("/services/$(args["logs"])/logs", public_key, secret_key)
|
||||
println(get(result, "logs", ""))
|
||||
return
|
||||
end
|
||||
|
||||
if args["sleep"] !== nothing
|
||||
api_request("/services/$(args["sleep"])/sleep", api_key, method="POST")
|
||||
api_request("/services/$(args["sleep"])/sleep", public_key, secret_key, method="POST")
|
||||
println("$(GREEN)Service sleeping: $(args["sleep"])$(RESET)")
|
||||
return
|
||||
end
|
||||
|
||||
if args["wake"] !== nothing
|
||||
api_request("/services/$(args["wake"])/wake", api_key, method="POST")
|
||||
api_request("/services/$(args["wake"])/wake", public_key, secret_key, method="POST")
|
||||
println("$(GREEN)Service waking: $(args["wake"])$(RESET)")
|
||||
return
|
||||
end
|
||||
|
||||
if args["destroy"] !== nothing
|
||||
api_request("/services/$(args["destroy"])", api_key, method="DELETE")
|
||||
api_request("/services/$(args["destroy"])", public_key, secret_key, method="DELETE")
|
||||
println("$(GREEN)Service destroyed: $(args["destroy"])$(RESET)")
|
||||
return
|
||||
end
|
||||
|
|
@ -291,7 +326,7 @@ function cmd_service(args)
|
|||
if args["dump-bootstrap"] !== nothing
|
||||
println(stderr, "Fetching bootstrap script from $(args["dump-bootstrap"])...")
|
||||
payload = Dict("command" => "cat /tmp/bootstrap.sh")
|
||||
result = api_request("/services/$(args["dump-bootstrap"])/execute", api_key, method="POST", data=payload)
|
||||
result = api_request("/services/$(args["dump-bootstrap"])/execute", public_key, secret_key, method="POST", data=payload)
|
||||
|
||||
if haskey(result, "stdout") && !isempty(result["stdout"])
|
||||
bootstrap = result["stdout"]
|
||||
|
|
@ -351,7 +386,7 @@ function cmd_service(args)
|
|||
payload["vcpu"] = args["vcpu"]
|
||||
end
|
||||
|
||||
result = api_request("/services", api_key, method="POST", data=payload)
|
||||
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"))")
|
||||
if haskey(result, "url")
|
||||
|
|
@ -452,7 +487,9 @@ function validate_key(api_key::String)
|
|||
end
|
||||
|
||||
function cmd_key(args)
|
||||
api_key = get_api_key(args["api-key"])
|
||||
(public_key, secret_key) = get_api_keys(args["api-key"])
|
||||
# For portal validation, we still use public_key as bearer token
|
||||
api_key = public_key
|
||||
|
||||
# Handle --extend flag
|
||||
if args["extend"]
|
||||
|
|
|
|||
129
un.js
129
un.js
|
|
@ -55,6 +55,7 @@ const fs = require('fs');
|
|||
const https = require('https');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
|
|
@ -80,13 +81,25 @@ const EXT_MAP = {
|
|||
".tcl": "tcl", ".raku": "raku", ".m": "objc",
|
||||
};
|
||||
|
||||
function getApiKey(argsKey) {
|
||||
const key = argsKey || process.env.UNSANDBOX_API_KEY;
|
||||
if (!key) {
|
||||
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`);
|
||||
process.exit(1);
|
||||
function getApiKeys(argsKey) {
|
||||
// Try new split key format first
|
||||
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
|
||||
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
|
||||
|
||||
// Fall back to old single key format for backwards compatibility
|
||||
if (!publicKey || !secretKey) {
|
||||
const oldKey = argsKey || process.env.UNSANDBOX_API_KEY;
|
||||
if (oldKey) {
|
||||
publicKey = oldKey;
|
||||
secretKey = oldKey;
|
||||
} else {
|
||||
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
|
||||
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return key;
|
||||
|
||||
return { publicKey, secretKey };
|
||||
}
|
||||
|
||||
function detectLanguage(filename) {
|
||||
|
|
@ -111,32 +124,45 @@ function detectLanguage(filename) {
|
|||
return lang;
|
||||
}
|
||||
|
||||
function apiRequest(endpoint, method = "GET", data = null, apiKey = null) {
|
||||
function apiRequest(endpoint, method = "GET", data = null, publicKey = null, secretKey = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(API_BASE + endpoint);
|
||||
|
||||
// Prepare body
|
||||
const body = data ? JSON.stringify(data) : "";
|
||||
|
||||
// Generate HMAC signature
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const signatureInput = `${timestamp}:${method}:${endpoint}:${body}`;
|
||||
const signature = crypto.createHmac('sha256', secretKey)
|
||||
.update(signatureInput)
|
||||
.digest('hex');
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Authorization': `Bearer ${publicKey}`,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Signature': signature,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 300000
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
let responseBody = '';
|
||||
res.on('data', chunk => responseBody += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
resolve(JSON.parse(responseBody));
|
||||
} catch (e) {
|
||||
resolve(body);
|
||||
resolve(responseBody);
|
||||
}
|
||||
} else {
|
||||
console.error(`${RED}Error: HTTP ${res.statusCode} - ${body}${RESET}`);
|
||||
console.error(`${RED}Error: HTTP ${res.statusCode} - ${responseBody}${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
|
@ -148,42 +174,55 @@ function apiRequest(endpoint, method = "GET", data = null, apiKey = null) {
|
|||
});
|
||||
|
||||
if (data) {
|
||||
req.write(JSON.stringify(data));
|
||||
req.write(body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function portalRequest(endpoint, method = "GET", data = null, apiKey = null) {
|
||||
function portalRequest(endpoint, method = "GET", data = null, publicKey = null, secretKey = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(PORTAL_BASE + endpoint);
|
||||
|
||||
// Prepare body
|
||||
const body = data ? JSON.stringify(data) : "";
|
||||
|
||||
// Generate HMAC signature
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const signatureInput = `${timestamp}:${method}:${endpoint}:${body}`;
|
||||
const signature = crypto.createHmac('sha256', secretKey)
|
||||
.update(signatureInput)
|
||||
.digest('hex');
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Authorization': `Bearer ${publicKey}`,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Signature': signature,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
let responseBody = '';
|
||||
res.on('data', chunk => responseBody += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try {
|
||||
resolve(JSON.parse(body));
|
||||
resolve(JSON.parse(responseBody));
|
||||
} catch (e) {
|
||||
resolve(body);
|
||||
resolve(responseBody);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const errorBody = JSON.parse(body);
|
||||
resolve({ error: errorBody.error || body, status: res.statusCode });
|
||||
const errorBody = JSON.parse(responseBody);
|
||||
resolve({ error: errorBody.error || responseBody, status: res.statusCode });
|
||||
} catch (e) {
|
||||
resolve({ error: body, status: res.statusCode });
|
||||
resolve({ error: responseBody, status: res.statusCode });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -194,7 +233,7 @@ function portalRequest(endpoint, method = "GET", data = null, apiKey = null) {
|
|||
});
|
||||
|
||||
if (data) {
|
||||
req.write(JSON.stringify(data));
|
||||
req.write(body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
|
|
@ -220,9 +259,9 @@ function openBrowser(url) {
|
|||
});
|
||||
}
|
||||
|
||||
async function validateKey(apiKey, shouldExtend = false) {
|
||||
async function validateKey(publicKey, secretKey, shouldExtend = false) {
|
||||
try {
|
||||
const result = await portalRequest("/keys/validate", "POST", {}, apiKey);
|
||||
const result = await portalRequest("/keys/validate", "POST", {}, publicKey, secretKey);
|
||||
|
||||
// Handle --extend flag first
|
||||
if (shouldExtend) {
|
||||
|
|
@ -265,12 +304,12 @@ async function validateKey(apiKey, shouldExtend = false) {
|
|||
}
|
||||
|
||||
async function cmdKey(args) {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
await validateKey(apiKey, args.extend);
|
||||
const { publicKey, secretKey } = getApiKeys(args.apiKey);
|
||||
await validateKey(publicKey, secretKey, args.extend);
|
||||
}
|
||||
|
||||
async function cmdExecute(args) {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
const { publicKey, secretKey } = getApiKeys(args.apiKey);
|
||||
|
||||
let code;
|
||||
try {
|
||||
|
|
@ -312,7 +351,7 @@ async function cmdExecute(args) {
|
|||
if (args.network) payload.network = args.network;
|
||||
if (args.vcpu) payload.vcpu = args.vcpu;
|
||||
|
||||
const result = await apiRequest("/execute", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/execute", "POST", payload, publicKey, secretKey);
|
||||
|
||||
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
|
||||
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
|
||||
|
|
@ -334,10 +373,10 @@ async function cmdExecute(args) {
|
|||
}
|
||||
|
||||
async function cmdSession(args) {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
const { publicKey, secretKey } = getApiKeys(args.apiKey);
|
||||
|
||||
if (args.list) {
|
||||
const result = await apiRequest("/sessions", "GET", null, apiKey);
|
||||
const result = await apiRequest("/sessions", "GET", null, publicKey, secretKey);
|
||||
const sessions = result.sessions || [];
|
||||
if (sessions.length === 0) {
|
||||
console.log("No active sessions");
|
||||
|
|
@ -351,7 +390,7 @@ async function cmdSession(args) {
|
|||
}
|
||||
|
||||
if (args.kill) {
|
||||
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, apiKey);
|
||||
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, publicKey, secretKey);
|
||||
console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -370,16 +409,16 @@ async function cmdSession(args) {
|
|||
if (args.audit) payload.audit = true;
|
||||
|
||||
console.log(`${YELLOW}Creating session...${RESET}`);
|
||||
const result = await apiRequest("/sessions", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/sessions", "POST", payload, publicKey, secretKey);
|
||||
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
|
||||
console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
|
||||
}
|
||||
|
||||
async function cmdService(args) {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
const { publicKey, secretKey } = getApiKeys(args.apiKey);
|
||||
|
||||
if (args.list) {
|
||||
const result = await apiRequest("/services", "GET", null, apiKey);
|
||||
const result = await apiRequest("/services", "GET", null, publicKey, secretKey);
|
||||
const services = result.services || [];
|
||||
if (services.length === 0) {
|
||||
console.log("No services");
|
||||
|
|
@ -395,44 +434,44 @@ async function cmdService(args) {
|
|||
}
|
||||
|
||||
if (args.info) {
|
||||
const result = await apiRequest(`/services/${args.info}`, "GET", null, apiKey);
|
||||
const result = await apiRequest(`/services/${args.info}`, "GET", null, publicKey, secretKey);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.logs) {
|
||||
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, apiKey);
|
||||
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, publicKey, secretKey);
|
||||
console.log(result.logs || "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.tail) {
|
||||
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, apiKey);
|
||||
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, publicKey, secretKey);
|
||||
console.log(result.logs || "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.sleep) {
|
||||
await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, apiKey);
|
||||
await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, publicKey, secretKey);
|
||||
console.log(`${GREEN}Service sleeping: ${args.sleep}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.wake) {
|
||||
await apiRequest(`/services/${args.wake}/wake`, "POST", null, apiKey);
|
||||
await apiRequest(`/services/${args.wake}/wake`, "POST", null, publicKey, secretKey);
|
||||
console.log(`${GREEN}Service waking: ${args.wake}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.destroy) {
|
||||
await apiRequest(`/services/${args.destroy}`, "DELETE", null, apiKey);
|
||||
await apiRequest(`/services/${args.destroy}`, "DELETE", null, publicKey, secretKey);
|
||||
console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.execute) {
|
||||
const payload = { command: args.command };
|
||||
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, apiKey);
|
||||
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, publicKey, secretKey);
|
||||
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
|
||||
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
|
||||
return;
|
||||
|
|
@ -441,7 +480,7 @@ async function cmdService(args) {
|
|||
if (args.dumpBootstrap) {
|
||||
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
|
||||
const payload = { command: "cat /tmp/bootstrap.sh" };
|
||||
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, apiKey);
|
||||
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, publicKey, secretKey);
|
||||
|
||||
if (result.stdout) {
|
||||
const bootstrap = result.stdout;
|
||||
|
|
@ -481,7 +520,7 @@ async function cmdService(args) {
|
|||
if (args.network) payload.network = args.network;
|
||||
if (args.vcpu) payload.vcpu = args.vcpu;
|
||||
|
||||
const result = await apiRequest("/services", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/services", "POST", payload, publicKey, secretKey);
|
||||
console.log(`${GREEN}Service created: ${result.id || 'N/A'}${RESET}`);
|
||||
console.log(`Name: ${result.name || 'N/A'}`);
|
||||
if (result.url) console.log(`URL: ${result.url}`);
|
||||
|
|
|
|||
117
un.kt
117
un.kt
|
|
@ -45,6 +45,8 @@ import java.net.HttpURLConnection
|
|||
import java.net.URL
|
||||
import java.util.Base64
|
||||
import kotlin.system.exitProcess
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
val API_BASE = "https://api.unsandbox.com"
|
||||
val PORTAL_BASE = "https://unsandbox.com"
|
||||
|
|
@ -123,7 +125,7 @@ fun main(args: Array<String>) {
|
|||
}
|
||||
|
||||
fun cmdExecute(args: Args) {
|
||||
val apiKey = getApiKey(args.apiKey)
|
||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
val code = File(args.sourceFile!!).readText()
|
||||
val language = detectLanguage(args.sourceFile!!)
|
||||
|
||||
|
|
@ -167,7 +169,7 @@ fun cmdExecute(args: Args) {
|
|||
payload["vcpu"] = args.vcpu
|
||||
}
|
||||
|
||||
val result = apiRequest("/execute", "POST", payload, apiKey)
|
||||
val result = apiRequest("/execute", "POST", payload, publicKey, secretKey)
|
||||
|
||||
val stdout = result["stdout"] as? String
|
||||
val stderr = result["stderr"] as? String
|
||||
|
|
@ -198,10 +200,10 @@ fun cmdExecute(args: Args) {
|
|||
}
|
||||
|
||||
fun cmdSession(args: Args) {
|
||||
val apiKey = getApiKey(args.apiKey)
|
||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
if (args.sessionList) {
|
||||
val result = apiRequest("/sessions", "GET", null, apiKey)
|
||||
val result = apiRequest("/sessions", "GET", null, publicKey, secretKey)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val sessions = result["sessions"] as? List<Map<String, Any>>
|
||||
if (sessions.isNullOrEmpty()) {
|
||||
|
|
@ -221,7 +223,7 @@ fun cmdSession(args: Args) {
|
|||
}
|
||||
|
||||
if (args.sessionKill != null) {
|
||||
apiRequest("/sessions/${args.sessionKill}", "DELETE", null, apiKey)
|
||||
apiRequest("/sessions/${args.sessionKill}", "DELETE", null, publicKey, secretKey)
|
||||
println("${GREEN}Session terminated: ${args.sessionKill}${RESET}")
|
||||
return
|
||||
}
|
||||
|
|
@ -237,16 +239,16 @@ fun cmdSession(args: Args) {
|
|||
}
|
||||
|
||||
println("${YELLOW}Creating session...${RESET}")
|
||||
val result = apiRequest("/sessions", "POST", payload, apiKey)
|
||||
val result = apiRequest("/sessions", "POST", payload, publicKey, secretKey)
|
||||
println("${GREEN}Session created: ${result["id"] ?: "N/A"}${RESET}")
|
||||
println("${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}")
|
||||
}
|
||||
|
||||
fun cmdService(args: Args) {
|
||||
val apiKey = getApiKey(args.apiKey)
|
||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
if (args.serviceList) {
|
||||
val result = apiRequest("/services", "GET", null, apiKey)
|
||||
val result = apiRequest("/services", "GET", null, publicKey, secretKey)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val services = result["services"] as? List<Map<String, Any>>
|
||||
if (services.isNullOrEmpty()) {
|
||||
|
|
@ -270,44 +272,44 @@ fun cmdService(args: Args) {
|
|||
}
|
||||
|
||||
if (args.serviceInfo != null) {
|
||||
val result = apiRequest("/services/${args.serviceInfo}", "GET", null, apiKey)
|
||||
val result = apiRequest("/services/${args.serviceInfo}", "GET", null, publicKey, secretKey)
|
||||
println(toJson(result))
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceLogs != null) {
|
||||
val result = apiRequest("/services/${args.serviceLogs}/logs", "GET", null, apiKey)
|
||||
val result = apiRequest("/services/${args.serviceLogs}/logs", "GET", null, publicKey, secretKey)
|
||||
println(result["logs"] ?: "")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceTail != null) {
|
||||
val result = apiRequest("/services/${args.serviceTail}/logs?lines=9000", "GET", null, apiKey)
|
||||
val result = apiRequest("/services/${args.serviceTail}/logs?lines=9000", "GET", null, publicKey, secretKey)
|
||||
println(result["logs"] ?: "")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceSleep != null) {
|
||||
apiRequest("/services/${args.serviceSleep}/sleep", "POST", null, apiKey)
|
||||
apiRequest("/services/${args.serviceSleep}/sleep", "POST", null, publicKey, secretKey)
|
||||
println("${GREEN}Service sleeping: ${args.serviceSleep}${RESET}")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceWake != null) {
|
||||
apiRequest("/services/${args.serviceWake}/wake", "POST", null, apiKey)
|
||||
apiRequest("/services/${args.serviceWake}/wake", "POST", null, publicKey, secretKey)
|
||||
println("${GREEN}Service waking: ${args.serviceWake}${RESET}")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceDestroy != null) {
|
||||
apiRequest("/services/${args.serviceDestroy}", "DELETE", null, apiKey)
|
||||
apiRequest("/services/${args.serviceDestroy}", "DELETE", null, publicKey, secretKey)
|
||||
println("${GREEN}Service destroyed: ${args.serviceDestroy}${RESET}")
|
||||
return
|
||||
}
|
||||
|
||||
if (args.serviceExecute != null) {
|
||||
val payload = mutableMapOf<String, Any>("command" to args.serviceCommand!!)
|
||||
val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, apiKey)
|
||||
val result = apiRequest("/services/${args.serviceExecute}/execute", "POST", payload, publicKey, secretKey)
|
||||
if (result.containsKey("stdout")) {
|
||||
val stdout = result["stdout"] as? String
|
||||
if (stdout != null && stdout.isNotEmpty()) {
|
||||
|
|
@ -326,7 +328,7 @@ fun cmdService(args: Args) {
|
|||
if (args.serviceDumpBootstrap != null) {
|
||||
System.err.println("Fetching bootstrap script from ${args.serviceDumpBootstrap}...")
|
||||
val payload = mutableMapOf<String, Any>("command" to "cat /tmp/bootstrap.sh")
|
||||
val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, apiKey)
|
||||
val result = apiRequest("/services/${args.serviceDumpBootstrap}/execute", "POST", payload, publicKey, secretKey)
|
||||
|
||||
val bootstrap = result["stdout"] as? String
|
||||
if (bootstrap != null && bootstrap.isNotEmpty()) {
|
||||
|
|
@ -368,7 +370,7 @@ fun cmdService(args: Args) {
|
|||
payload["vcpu"] = args.vcpu
|
||||
}
|
||||
|
||||
val result = apiRequest("/services", "POST", payload, apiKey)
|
||||
val result = apiRequest("/services", "POST", payload, publicKey, secretKey)
|
||||
println("${GREEN}Service created: ${result["id"] ?: "N/A"}${RESET}")
|
||||
println("Name: ${result["name"] ?: "N/A"}")
|
||||
if (result.containsKey("url")) {
|
||||
|
|
@ -382,21 +384,21 @@ fun cmdService(args: Args) {
|
|||
}
|
||||
|
||||
fun cmdKey(args: Args) {
|
||||
val apiKey = getApiKey(args.apiKey)
|
||||
val (publicKey, secretKey) = getApiKeys(args.apiKey)
|
||||
|
||||
val result = validateKey(apiKey)
|
||||
val result = validateKey(publicKey, secretKey)
|
||||
val valid = result["valid"] as? Boolean ?: false
|
||||
val expired = result["expired"] as? Boolean ?: false
|
||||
val publicKey = result["public_key"] as? String ?: ""
|
||||
val pubKey = result["public_key"] as? String ?: ""
|
||||
val tier = result["tier"] as? String ?: ""
|
||||
val expiresAt = result["expires_at"] as? String ?: ""
|
||||
|
||||
if (args.keyExtend) {
|
||||
if (publicKey.isEmpty()) {
|
||||
if (pubKey.isEmpty()) {
|
||||
System.err.println("${RED}Error: Could not retrieve public key${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
val extendUrl = "$PORTAL_BASE/keys/extend?pk=$publicKey"
|
||||
val extendUrl = "$PORTAL_BASE/keys/extend?pk=$pubKey"
|
||||
println("${YELLOW}Opening browser to extend key...${RESET}")
|
||||
println(extendUrl)
|
||||
|
||||
|
|
@ -418,13 +420,13 @@ fun cmdKey(args: Args) {
|
|||
|
||||
if (expired) {
|
||||
println("${RED}Status: Expired${RESET}")
|
||||
println("Public Key: $publicKey")
|
||||
println("Public Key: $pubKey")
|
||||
println("Tier: $tier")
|
||||
println("Expired: $expiresAt")
|
||||
println("${YELLOW}To renew: Visit $PORTAL_BASE/keys/extend${RESET}")
|
||||
} else if (valid) {
|
||||
println("${GREEN}Status: Valid${RESET}")
|
||||
println("Public Key: $publicKey")
|
||||
println("Public Key: $pubKey")
|
||||
println("Tier: $tier")
|
||||
println("Expires: $expiresAt")
|
||||
} else {
|
||||
|
|
@ -433,12 +435,21 @@ fun cmdKey(args: Args) {
|
|||
}
|
||||
}
|
||||
|
||||
fun validateKey(apiKey: String): Map<String, Any> {
|
||||
val url = URL("$PORTAL_BASE/keys/validate")
|
||||
fun validateKey(publicKey: String?, secretKey: String): Map<String, Any> {
|
||||
val timestamp = System.currentTimeMillis() / 1000
|
||||
val method = "POST"
|
||||
val path = "/keys/validate"
|
||||
val body = ""
|
||||
val signatureData = "$timestamp:$method:$path:$body"
|
||||
val signature = hmacSha256(secretKey, signatureData)
|
||||
|
||||
val url = URL("$PORTAL_BASE$path")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
|
||||
connection.requestMethod = "POST"
|
||||
connection.setRequestProperty("Authorization", "Bearer $apiKey")
|
||||
connection.requestMethod = method
|
||||
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
|
||||
connection.setRequestProperty("X-Timestamp", timestamp.toString())
|
||||
connection.setRequestProperty("X-Signature", signature)
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.connectTimeout = 30000
|
||||
connection.readTimeout = 30000
|
||||
|
|
@ -452,13 +463,39 @@ fun validateKey(apiKey: String): Map<String, Any> {
|
|||
return parseJson(response)
|
||||
}
|
||||
|
||||
fun getApiKey(argsKey: String?): String {
|
||||
val key = argsKey ?: System.getenv("UNSANDBOX_API_KEY")
|
||||
if (key.isNullOrEmpty()) {
|
||||
System.err.println("${RED}Error: UNSANDBOX_API_KEY not set${RESET}")
|
||||
fun getApiKeys(argsKey: String?): Pair<String?, String> {
|
||||
var publicKey: String? = null
|
||||
var secretKey: String? = null
|
||||
|
||||
if (argsKey != null) {
|
||||
secretKey = argsKey
|
||||
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
} else {
|
||||
publicKey = System.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
secretKey = System.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if (publicKey == null || secretKey == null) {
|
||||
val apiKey = System.getenv("UNSANDBOX_API_KEY")
|
||||
if (apiKey != null && apiKey.isNotEmpty()) {
|
||||
secretKey = apiKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (secretKey.isNullOrEmpty()) {
|
||||
System.err.println("${RED}Error: UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set${RESET}")
|
||||
exitProcess(1)
|
||||
}
|
||||
return key
|
||||
|
||||
return Pair(publicKey, secretKey)
|
||||
}
|
||||
|
||||
fun hmacSha256(secretKey: String, data: String): String {
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
val keySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), "HmacSHA256")
|
||||
mac.init(keySpec)
|
||||
val hash = mac.doFinal(data.toByteArray(Charsets.UTF_8))
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
fun detectLanguage(filename: String): String {
|
||||
|
|
@ -469,20 +506,26 @@ fun detectLanguage(filename: String): String {
|
|||
return EXT_MAP[".$ext"] ?: throw RuntimeException("Unsupported file extension: .$ext")
|
||||
}
|
||||
|
||||
fun apiRequest(endpoint: String, method: String, data: Map<String, Any>?, apiKey: String): Map<String, Any> {
|
||||
fun apiRequest(endpoint: String, method: String, data: Map<String, Any>?, publicKey: String?, secretKey: String): Map<String, Any> {
|
||||
val timestamp = System.currentTimeMillis() / 1000
|
||||
val body = if (data != null) toJson(data) else ""
|
||||
val signatureData = "$timestamp:$method:$endpoint:$body"
|
||||
val signature = hmacSha256(secretKey, signatureData)
|
||||
|
||||
val url = URL(API_BASE + endpoint)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
|
||||
connection.requestMethod = method
|
||||
connection.setRequestProperty("Authorization", "Bearer $apiKey")
|
||||
connection.setRequestProperty("Authorization", "Bearer ${publicKey ?: secretKey}")
|
||||
connection.setRequestProperty("X-Timestamp", timestamp.toString())
|
||||
connection.setRequestProperty("X-Signature", signature)
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.connectTimeout = 30000
|
||||
connection.readTimeout = 300000
|
||||
|
||||
if (data != null) {
|
||||
connection.doOutput = true
|
||||
val json = toJson(data)
|
||||
connection.outputStream.use { it.write(json.toByteArray()) }
|
||||
connection.outputStream.use { it.write(body.toByteArray()) }
|
||||
}
|
||||
|
||||
if (connection.responseCode !in 200..299) {
|
||||
|
|
|
|||
77
un.lisp
77
un.lisp
|
|
@ -104,38 +104,75 @@
|
|||
(defun curl-post (api-key endpoint json-data)
|
||||
(let ((tmp-file (write-temp-file json-data)))
|
||||
(unwind-protect
|
||||
(run-curl (list "curl" "-s" "-X" "POST"
|
||||
(format nil "https://api.unsandbox.com~a" endpoint)
|
||||
"-H" "Content-Type: application/json"
|
||||
"-H" (format nil "Authorization: Bearer ~a" api-key)
|
||||
"-d" (format nil "@~a" tmp-file)))
|
||||
(destructuring-bind (public-key secret-key) (get-api-keys)
|
||||
(let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
|
||||
(base-args (list "curl" "-s" "-X" "POST"
|
||||
(format nil "https://api.unsandbox.com~a" endpoint)
|
||||
"-H" "Content-Type: application/json")))
|
||||
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
|
||||
(delete-file tmp-file))))
|
||||
|
||||
(defun curl-get (api-key endpoint)
|
||||
(run-curl (list "curl" "-s"
|
||||
(format nil "https://api.unsandbox.com~a" endpoint)
|
||||
"-H" (format nil "Authorization: Bearer ~a" api-key))))
|
||||
(destructuring-bind (public-key secret-key) (get-api-keys)
|
||||
(let ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
|
||||
(base-args (list "curl" "-s"
|
||||
(format nil "https://api.unsandbox.com~a" endpoint))))
|
||||
(run-curl (append base-args auth-headers)))))
|
||||
|
||||
(defun curl-delete (api-key endpoint)
|
||||
(run-curl (list "curl" "-s" "-X" "DELETE"
|
||||
(format nil "https://api.unsandbox.com~a" endpoint)
|
||||
"-H" (format nil "Authorization: Bearer ~a" api-key))))
|
||||
(destructuring-bind (public-key secret-key) (get-api-keys)
|
||||
(let ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
|
||||
(base-args (list "curl" "-s" "-X" "DELETE"
|
||||
(format nil "https://api.unsandbox.com~a" endpoint))))
|
||||
(run-curl (append base-args auth-headers)))))
|
||||
|
||||
(defun curl-post-portal (api-key endpoint json-data)
|
||||
(let ((tmp-file (write-temp-file json-data)))
|
||||
(unwind-protect
|
||||
(run-curl (list "curl" "-s" "-X" "POST"
|
||||
(format nil "~a~a" *portal-base* endpoint)
|
||||
"-H" "Content-Type: application/json"
|
||||
"-H" (format nil "Authorization: Bearer ~a" api-key)
|
||||
"-d" (format nil "@~a" tmp-file)))
|
||||
(destructuring-bind (public-key secret-key) (get-api-keys)
|
||||
(let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
|
||||
(base-args (list "curl" "-s" "-X" "POST"
|
||||
(format nil "~a~a" *portal-base* endpoint)
|
||||
"-H" "Content-Type: application/json")))
|
||||
(run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file))))))
|
||||
(delete-file tmp-file))))
|
||||
|
||||
(defun get-api-keys ()
|
||||
(let ((public-key (uiop:getenv "UNSANDBOX_PUBLIC_KEY"))
|
||||
(secret-key (uiop:getenv "UNSANDBOX_SECRET_KEY"))
|
||||
(api-key (uiop:getenv "UNSANDBOX_API_KEY")))
|
||||
(cond
|
||||
((and public-key secret-key) (list public-key secret-key))
|
||||
(api-key (list api-key nil))
|
||||
(t (progn
|
||||
(format t "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)~%")
|
||||
(uiop:quit 1))))))
|
||||
|
||||
(defun get-api-key ()
|
||||
(or (uiop:getenv "UNSANDBOX_API_KEY")
|
||||
(progn
|
||||
(format t "Error: UNSANDBOX_API_KEY not set~%")
|
||||
(uiop:quit 1))))
|
||||
(first (get-api-keys)))
|
||||
|
||||
(defun hmac-sha256 (secret message)
|
||||
"Compute HMAC-SHA256 using openssl command"
|
||||
(let* ((secret-escaped (uiop:escape-sh-token secret))
|
||||
(message-escaped (uiop:escape-sh-token message))
|
||||
(cmd (format nil "echo -n ~a | openssl dgst -sha256 -hmac ~a | awk '{print $2}'"
|
||||
message-escaped secret-escaped))
|
||||
(result (string-trim '(#\Space #\Tab #\Newline #\Return)
|
||||
(uiop:run-program cmd :output :string))))
|
||||
result))
|
||||
|
||||
(defun make-signature (secret-key timestamp method path body)
|
||||
(let ((message (format nil "~a:~a:~a:~a" timestamp method path body)))
|
||||
(hmac-sha256 secret-key message)))
|
||||
|
||||
(defun build-auth-headers (public-key secret-key method path body)
|
||||
(if secret-key
|
||||
(let* ((timestamp (write-to-string (floor (get-universal-time))))
|
||||
(signature (make-signature secret-key timestamp method path body)))
|
||||
(list "-H" (format nil "Authorization: Bearer ~a" public-key)
|
||||
"-H" (format nil "X-Timestamp: ~a" timestamp)
|
||||
"-H" (format nil "X-Signature: ~a" signature)))
|
||||
(list "-H" (format nil "Authorization: Bearer ~a" public-key))))
|
||||
|
||||
(defun execute-cmd (file)
|
||||
(let* ((api-key (get-api-key))
|
||||
|
|
|
|||
136
un.lua
136
un.lua
|
|
@ -76,13 +76,23 @@ local EXT_MAP = {
|
|||
[".tcl"] = "tcl", [".raku"] = "raku", [".m"] = "objc"
|
||||
}
|
||||
|
||||
local function get_api_key(args_key)
|
||||
local key = args_key or os.getenv("UNSANDBOX_API_KEY")
|
||||
if not key then
|
||||
io.stderr:write(RED .. "Error: UNSANDBOX_API_KEY not set" .. RESET .. "\n")
|
||||
os.exit(1)
|
||||
local function get_api_keys(args_key)
|
||||
local public_key = os.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
local secret_key = os.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
if not public_key or not secret_key then
|
||||
local old_key = args_key or os.getenv("UNSANDBOX_API_KEY")
|
||||
if old_key then
|
||||
public_key = old_key
|
||||
secret_key = old_key
|
||||
else
|
||||
io.stderr:write(RED .. "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" .. RESET .. "\n")
|
||||
io.stderr:write(RED .. " (or legacy UNSANDBOX_API_KEY for backwards compatibility)" .. RESET .. "\n")
|
||||
os.exit(1)
|
||||
end
|
||||
end
|
||||
return key
|
||||
|
||||
return {public_key = public_key, secret_key = secret_key}
|
||||
end
|
||||
|
||||
local function detect_language(filename)
|
||||
|
|
@ -115,20 +125,46 @@ local function shell_escape(str)
|
|||
return "'" .. str:gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
local function api_request(endpoint, method, data, api_key)
|
||||
local function api_request(endpoint, method, data, keys)
|
||||
method = method or "GET"
|
||||
local url = API_BASE .. endpoint
|
||||
local tmpfile = os.tmpname()
|
||||
|
||||
-- Generate timestamp and signature
|
||||
local timestamp = tostring(os.time())
|
||||
local body = data and json.encode(data) or ""
|
||||
|
||||
-- Parse URL to get path
|
||||
local path = endpoint
|
||||
|
||||
-- Create HMAC signature using openssl command
|
||||
local message = timestamp .. ":" .. method .. ":" .. path .. ":" .. body
|
||||
local sig_tmpfile = os.tmpname()
|
||||
local msg_tmpfile = os.tmpname()
|
||||
|
||||
-- Write message to temp file
|
||||
local f = io.open(msg_tmpfile, "w")
|
||||
f:write(message)
|
||||
f:close()
|
||||
|
||||
-- Generate HMAC using openssl
|
||||
local hmac_cmd = "openssl dgst -sha256 -hmac " .. shell_escape(keys.secret_key) .. " -hex " .. shell_escape(msg_tmpfile) .. " | awk '{print $2}'"
|
||||
local sig_handle = io.popen(hmac_cmd)
|
||||
local signature = sig_handle:read("*a"):gsub("%s+$", "")
|
||||
sig_handle:close()
|
||||
os.remove(msg_tmpfile)
|
||||
|
||||
local cmd = "curl -s -X " .. method .. " " .. shell_escape(url) ..
|
||||
" -H 'Authorization: Bearer " .. api_key .. "'" ..
|
||||
" -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
|
||||
" -H 'X-Timestamp: " .. timestamp .. "'" ..
|
||||
" -H 'X-Signature: " .. signature .. "'" ..
|
||||
" -H 'Content-Type: application/json'"
|
||||
|
||||
local data_file
|
||||
if data then
|
||||
local payload = json.encode(data)
|
||||
local data_file = os.tmpname()
|
||||
data_file = os.tmpname()
|
||||
local f = io.open(data_file, "w")
|
||||
f:write(payload)
|
||||
f:write(body)
|
||||
f:close()
|
||||
cmd = cmd .. " -d @" .. shell_escape(data_file)
|
||||
end
|
||||
|
|
@ -144,7 +180,7 @@ local function api_request(endpoint, method, data, api_key)
|
|||
file:close()
|
||||
os.remove(tmpfile)
|
||||
|
||||
if data then
|
||||
if data_file then
|
||||
os.remove(data_file)
|
||||
end
|
||||
|
||||
|
|
@ -198,7 +234,7 @@ local function base64_decode(data)
|
|||
end
|
||||
|
||||
local function cmd_execute(options)
|
||||
local api_key = get_api_key(options.api_key)
|
||||
local keys = get_api_keys(options.api_key)
|
||||
local code = read_file(options.source_file)
|
||||
local language = detect_language(options.source_file)
|
||||
|
||||
|
|
@ -233,7 +269,7 @@ local function cmd_execute(options)
|
|||
if options.network then payload.network = options.network end
|
||||
if options.vcpu then payload.vcpu = options.vcpu end
|
||||
|
||||
local result = api_request("/execute", "POST", payload, api_key)
|
||||
local result = api_request("/execute", "POST", payload, keys)
|
||||
|
||||
if result.stdout then
|
||||
io.write(BLUE .. result.stdout .. RESET)
|
||||
|
|
@ -261,10 +297,10 @@ local function cmd_execute(options)
|
|||
end
|
||||
|
||||
local function cmd_session(options)
|
||||
local api_key = get_api_key(options.api_key)
|
||||
local keys = get_api_keys(options.api_key)
|
||||
|
||||
if options.list then
|
||||
local result = api_request("/sessions", "GET", nil, api_key)
|
||||
local result = api_request("/sessions", "GET", nil, keys)
|
||||
local sessions = result.sessions or {}
|
||||
if #sessions == 0 then
|
||||
print("No active sessions")
|
||||
|
|
@ -280,7 +316,7 @@ local function cmd_session(options)
|
|||
end
|
||||
|
||||
if options.kill then
|
||||
api_request("/sessions/" .. options.kill, "DELETE", nil, api_key)
|
||||
api_request("/sessions/" .. options.kill, "DELETE", nil, keys)
|
||||
print(GREEN .. "Session terminated: " .. options.kill .. RESET)
|
||||
return
|
||||
end
|
||||
|
|
@ -299,21 +335,39 @@ local function cmd_session(options)
|
|||
if options.audit then payload.audit = true end
|
||||
|
||||
print(YELLOW .. "Creating session..." .. RESET)
|
||||
local result = api_request("/sessions", "POST", payload, api_key)
|
||||
local result = api_request("/sessions", "POST", payload, keys)
|
||||
print(GREEN .. "Session created: " .. (result.id or "N/A") .. RESET)
|
||||
print(YELLOW .. "(Interactive sessions require WebSocket - use un2 for full support)" .. RESET)
|
||||
end
|
||||
|
||||
local function cmd_key(options)
|
||||
local api_key = get_api_key(options.api_key)
|
||||
local keys = get_api_keys(options.api_key)
|
||||
|
||||
if options.extend then
|
||||
-- Get public_key from validation response
|
||||
local url = PORTAL_BASE .. "/keys/validate"
|
||||
local tmpfile = os.tmpname()
|
||||
|
||||
local timestamp = tostring(os.time())
|
||||
local body = ""
|
||||
local path = "/keys/validate"
|
||||
local message = timestamp .. ":POST:" .. path .. ":" .. body
|
||||
local msg_tmpfile = os.tmpname()
|
||||
|
||||
local f = io.open(msg_tmpfile, "w")
|
||||
f:write(message)
|
||||
f:close()
|
||||
|
||||
local hmac_cmd = "openssl dgst -sha256 -hmac " .. shell_escape(keys.secret_key) .. " -hex " .. shell_escape(msg_tmpfile) .. " | awk '{print $2}'"
|
||||
local sig_handle = io.popen(hmac_cmd)
|
||||
local signature = sig_handle:read("*a"):gsub("%s+$", "")
|
||||
sig_handle:close()
|
||||
os.remove(msg_tmpfile)
|
||||
|
||||
local cmd = "curl -s -X POST " .. shell_escape(url) ..
|
||||
" -H 'Authorization: Bearer " .. api_key .. "'" ..
|
||||
" -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
|
||||
" -H 'X-Timestamp: " .. timestamp .. "'" ..
|
||||
" -H 'X-Signature: " .. signature .. "'" ..
|
||||
" -H 'Content-Type: application/json'" ..
|
||||
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
|
||||
|
||||
|
|
@ -351,8 +405,26 @@ local function cmd_key(options)
|
|||
local url = PORTAL_BASE .. "/keys/validate"
|
||||
local tmpfile = os.tmpname()
|
||||
|
||||
local timestamp = tostring(os.time())
|
||||
local body = ""
|
||||
local path = "/keys/validate"
|
||||
local message = timestamp .. ":POST:" .. path .. ":" .. body
|
||||
local msg_tmpfile = os.tmpname()
|
||||
|
||||
local f = io.open(msg_tmpfile, "w")
|
||||
f:write(message)
|
||||
f:close()
|
||||
|
||||
local hmac_cmd = "openssl dgst -sha256 -hmac " .. shell_escape(keys.secret_key) .. " -hex " .. shell_escape(msg_tmpfile) .. " | awk '{print $2}'"
|
||||
local sig_handle = io.popen(hmac_cmd)
|
||||
local signature = sig_handle:read("*a"):gsub("%s+$", "")
|
||||
sig_handle:close()
|
||||
os.remove(msg_tmpfile)
|
||||
|
||||
local cmd = "curl -s -X POST " .. shell_escape(url) ..
|
||||
" -H 'Authorization: Bearer " .. api_key .. "'" ..
|
||||
" -H 'Authorization: Bearer " .. keys.public_key .. "'" ..
|
||||
" -H 'X-Timestamp: " .. timestamp .. "'" ..
|
||||
" -H 'X-Signature: " .. signature .. "'" ..
|
||||
" -H 'Content-Type: application/json'" ..
|
||||
" -w '\\n%{http_code}' -o " .. shell_escape(tmpfile)
|
||||
|
||||
|
|
@ -390,10 +462,10 @@ local function cmd_key(options)
|
|||
end
|
||||
|
||||
local function cmd_service(options)
|
||||
local api_key = get_api_key(options.api_key)
|
||||
local keys = get_api_keys(options.api_key)
|
||||
|
||||
if options.list then
|
||||
local result = api_request("/services", "GET", nil, api_key)
|
||||
local result = api_request("/services", "GET", nil, keys)
|
||||
local services = result.services or {}
|
||||
if #services == 0 then
|
||||
print("No services")
|
||||
|
|
@ -411,44 +483,44 @@ local function cmd_service(options)
|
|||
end
|
||||
|
||||
if options.info then
|
||||
local result = api_request("/services/" .. options.info, "GET", nil, api_key)
|
||||
local result = api_request("/services/" .. options.info, "GET", nil, keys)
|
||||
print(json.encode(result))
|
||||
return
|
||||
end
|
||||
|
||||
if options.logs then
|
||||
local result = api_request("/services/" .. options.logs .. "/logs", "GET", nil, api_key)
|
||||
local result = api_request("/services/" .. options.logs .. "/logs", "GET", nil, keys)
|
||||
print(result.logs or "")
|
||||
return
|
||||
end
|
||||
|
||||
if options.tail then
|
||||
local result = api_request("/services/" .. options.tail .. "/logs?lines=9000", "GET", nil, api_key)
|
||||
local result = api_request("/services/" .. options.tail .. "/logs?lines=9000", "GET", nil, keys)
|
||||
print(result.logs or "")
|
||||
return
|
||||
end
|
||||
|
||||
if options.sleep then
|
||||
api_request("/services/" .. options.sleep .. "/sleep", "POST", nil, api_key)
|
||||
api_request("/services/" .. options.sleep .. "/sleep", "POST", nil, keys)
|
||||
print(GREEN .. "Service sleeping: " .. options.sleep .. RESET)
|
||||
return
|
||||
end
|
||||
|
||||
if options.wake then
|
||||
api_request("/services/" .. options.wake .. "/wake", "POST", nil, api_key)
|
||||
api_request("/services/" .. options.wake .. "/wake", "POST", nil, keys)
|
||||
print(GREEN .. "Service waking: " .. options.wake .. RESET)
|
||||
return
|
||||
end
|
||||
|
||||
if options.destroy then
|
||||
api_request("/services/" .. options.destroy, "DELETE", nil, api_key)
|
||||
api_request("/services/" .. options.destroy, "DELETE", nil, keys)
|
||||
print(GREEN .. "Service destroyed: " .. options.destroy .. RESET)
|
||||
return
|
||||
end
|
||||
|
||||
if options.execute then
|
||||
local payload = { command = options.command }
|
||||
local result = api_request("/services/" .. options.execute .. "/execute", "POST", payload, api_key)
|
||||
local result = api_request("/services/" .. options.execute .. "/execute", "POST", payload, keys)
|
||||
if result.stdout then io.write(BLUE .. result.stdout .. RESET) end
|
||||
if result.stderr then io.stderr:write(RED .. result.stderr .. RESET) end
|
||||
return
|
||||
|
|
@ -457,7 +529,7 @@ local function cmd_service(options)
|
|||
if options.dump_bootstrap then
|
||||
io.stderr:write("Fetching bootstrap script from " .. options.dump_bootstrap .. "...\n")
|
||||
local payload = { command = "cat /tmp/bootstrap.sh" }
|
||||
local result = api_request("/services/" .. options.dump_bootstrap .. "/execute", "POST", payload, api_key)
|
||||
local result = api_request("/services/" .. options.dump_bootstrap .. "/execute", "POST", payload, keys)
|
||||
|
||||
if result.stdout then
|
||||
local bootstrap = result.stdout
|
||||
|
|
@ -514,7 +586,7 @@ local function cmd_service(options)
|
|||
if options.network then payload.network = options.network end
|
||||
if options.vcpu then payload.vcpu = options.vcpu end
|
||||
|
||||
local result = api_request("/services", "POST", payload, api_key)
|
||||
local result = api_request("/services", "POST", payload, keys)
|
||||
print(GREEN .. "Service created: " .. (result.id or "N/A") .. RESET)
|
||||
print("Name: " .. (result.name or "N/A"))
|
||||
if result.url then print("URL: " .. result.url) end
|
||||
|
|
|
|||
101
un.m
101
un.m
|
|
@ -41,6 +41,7 @@
|
|||
// Full-featured CLI matching un.c/un.py capabilities
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CommonCrypto/CommonHMAC.h>
|
||||
|
||||
static NSString* API_BASE = @"https://api.unsandbox.com";
|
||||
static NSString* PORTAL_BASE = @"https://unsandbox.com";
|
||||
|
|
@ -69,13 +70,47 @@ NSDictionary* getExtMap() {
|
|||
};
|
||||
}
|
||||
|
||||
NSString* getApiKey() {
|
||||
NSString* key = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"];
|
||||
if (!key) {
|
||||
fprintf(stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||
void getApiKeys(NSString** publicKey, NSString** secretKey) {
|
||||
// Try new-style keys first
|
||||
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
|
||||
*secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
|
||||
|
||||
// Fall back to old-style single key
|
||||
if (!*publicKey || [*publicKey length] == 0) {
|
||||
NSString* oldKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"];
|
||||
if (!oldKey) {
|
||||
fprintf(stderr, "%sError: UNSANDBOX_PUBLIC_KEY/UNSANDBOX_SECRET_KEY or UNSANDBOX_API_KEY not set%s\n",
|
||||
[RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
*publicKey = oldKey;
|
||||
*secretKey = oldKey;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!*secretKey || [*secretKey length] == 0) {
|
||||
fprintf(stderr, "%sError: UNSANDBOX_SECRET_KEY not set%s\n", [RED UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
NSString* hmacSha256Hex(NSString* key, NSString* message) {
|
||||
const char* cKey = [key UTF8String];
|
||||
const char* cMessage = [message UTF8String];
|
||||
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
||||
|
||||
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cMessage, strlen(cMessage), digest);
|
||||
|
||||
NSMutableString* hex = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
||||
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
||||
[hex appendFormat:@"%02x", digest[i]];
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
NSString* computeSignature(NSString* secretKey, long timestamp, NSString* method, NSString* path, NSString* body) {
|
||||
NSString* message = [NSString stringWithFormat:@"%ld:%@:%@:%@", timestamp, method, path, body];
|
||||
return hmacSha256Hex(secretKey, message);
|
||||
}
|
||||
|
||||
NSString* detectLanguage(NSString* filename) {
|
||||
|
|
@ -92,15 +127,15 @@ NSString* detectLanguage(NSString* filename) {
|
|||
return language;
|
||||
}
|
||||
|
||||
NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* data, NSString* apiKey) {
|
||||
NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* data, NSString* publicKey, NSString* secretKey) {
|
||||
NSString* urlString = [API_BASE stringByAppendingString:endpoint];
|
||||
NSURL* url = [NSURL URLWithString:urlString];
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||
[request setHTTPMethod:method];
|
||||
[request setValue:[@"Bearer " stringByAppendingString:apiKey] forHTTPHeaderField:@"Authorization"];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
[request setTimeoutInterval:300];
|
||||
|
||||
// Prepare body
|
||||
NSString* bodyString = @"";
|
||||
if (data) {
|
||||
NSError* error = nil;
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
|
||||
|
|
@ -109,9 +144,20 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat
|
|||
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
|
||||
exit(1);
|
||||
}
|
||||
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
[request setHTTPBody:jsonData];
|
||||
}
|
||||
|
||||
// Generate timestamp and signature
|
||||
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
||||
NSString* signature = computeSignature(secretKey, timestamp, method, endpoint, bodyString);
|
||||
|
||||
// Set headers
|
||||
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
|
||||
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
|
||||
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
NSHTTPURLResponse* response = nil;
|
||||
NSError* error = nil;
|
||||
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
|
||||
|
|
@ -139,7 +185,8 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat
|
|||
}
|
||||
|
||||
void cmdExecute(NSArray* args) {
|
||||
NSString* apiKey = getApiKey();
|
||||
NSString* publicKey, *secretKey;
|
||||
getApiKeys(&publicKey, &secretKey);
|
||||
NSString* sourceFile = nil;
|
||||
NSMutableDictionary* envVars = [NSMutableDictionary dictionary];
|
||||
NSMutableArray* inputFiles = [NSMutableArray array];
|
||||
|
|
@ -233,7 +280,7 @@ void cmdExecute(NSArray* args) {
|
|||
}
|
||||
|
||||
// Execute
|
||||
NSDictionary* result = apiRequest(@"/execute", @"POST", payload, apiKey);
|
||||
NSDictionary* result = apiRequest(@"/execute", @"POST", payload, publicKey, secretKey);
|
||||
|
||||
// Print output
|
||||
NSString* stdoutText = result[@"stdout"] ?: @"";
|
||||
|
|
@ -361,7 +408,8 @@ void validateKey(NSString* apiKey, BOOL shouldExtend) {
|
|||
}
|
||||
|
||||
void cmdKey(NSArray* args) {
|
||||
NSString* apiKey = getApiKey();
|
||||
NSString* publicKey, *secretKey;
|
||||
getApiKeys(&publicKey, &secretKey);
|
||||
BOOL shouldExtend = NO;
|
||||
|
||||
for (NSString* arg in args) {
|
||||
|
|
@ -370,11 +418,13 @@ void cmdKey(NSArray* args) {
|
|||
}
|
||||
}
|
||||
|
||||
validateKey(apiKey, shouldExtend);
|
||||
// For portal validation, we use public key as bearer token
|
||||
validateKey(publicKey, shouldExtend);
|
||||
}
|
||||
|
||||
void cmdSession(NSArray* args) {
|
||||
NSString* apiKey = getApiKey();
|
||||
NSString* publicKey, *secretKey;
|
||||
getApiKeys(&publicKey, &secretKey);
|
||||
BOOL listMode = NO;
|
||||
NSString* killId = nil;
|
||||
NSString* shell = nil;
|
||||
|
|
@ -398,7 +448,7 @@ void cmdSession(NSArray* args) {
|
|||
}
|
||||
|
||||
if (listMode) {
|
||||
NSDictionary* result = apiRequest(@"/sessions", @"GET", nil, apiKey);
|
||||
NSDictionary* result = apiRequest(@"/sessions", @"GET", nil, publicKey, secretKey);
|
||||
NSArray* sessions = result[@"sessions"];
|
||||
if ([sessions count] == 0) {
|
||||
printf("No active sessions\n");
|
||||
|
|
@ -417,7 +467,7 @@ void cmdSession(NSArray* args) {
|
|||
|
||||
if (killId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@", killId];
|
||||
apiRequest(endpoint, @"DELETE", nil, apiKey);
|
||||
apiRequest(endpoint, @"DELETE", nil, publicKey, secretKey);
|
||||
printf("%sSession terminated: %s%s\n", [GREEN UTF8String], [killId UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -430,14 +480,15 @@ void cmdSession(NSArray* args) {
|
|||
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
||||
|
||||
printf("%sCreating session...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
|
||||
NSDictionary* result = apiRequest(@"/sessions", @"POST", payload, apiKey);
|
||||
NSDictionary* result = apiRequest(@"/sessions", @"POST", payload, publicKey, secretKey);
|
||||
printf("%sSession created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
|
||||
printf("%s(Interactive sessions require WebSocket - use un2 for full support)%s\n",
|
||||
[YELLOW UTF8String], [RESET UTF8String]);
|
||||
}
|
||||
|
||||
void cmdService(NSArray* args) {
|
||||
NSString* apiKey = getApiKey();
|
||||
NSString* publicKey, *secretKey;
|
||||
getApiKeys(&publicKey, &secretKey);
|
||||
BOOL listMode = NO;
|
||||
NSString* infoId = nil;
|
||||
NSString* logsId = nil;
|
||||
|
|
@ -488,7 +539,7 @@ void cmdService(NSArray* args) {
|
|||
}
|
||||
|
||||
if (listMode) {
|
||||
NSDictionary* result = apiRequest(@"/services", @"GET", nil, apiKey);
|
||||
NSDictionary* result = apiRequest(@"/services", @"GET", nil, publicKey, secretKey);
|
||||
NSArray* services = result[@"services"];
|
||||
if ([services count] == 0) {
|
||||
printf("No services\n");
|
||||
|
|
@ -512,7 +563,7 @@ void cmdService(NSArray* args) {
|
|||
|
||||
if (infoId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", infoId];
|
||||
NSDictionary* result = apiRequest(endpoint, @"GET", nil, apiKey);
|
||||
NSDictionary* result = apiRequest(endpoint, @"GET", nil, publicKey, secretKey);
|
||||
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
||||
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
printf("%s\n", [jsonString UTF8String]);
|
||||
|
|
@ -521,28 +572,28 @@ void cmdService(NSArray* args) {
|
|||
|
||||
if (logsId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/logs", logsId];
|
||||
NSDictionary* result = apiRequest(endpoint, @"GET", nil, apiKey);
|
||||
NSDictionary* result = apiRequest(endpoint, @"GET", nil, publicKey, secretKey);
|
||||
printf("%s", [result[@"logs"] UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sleepId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/sleep", sleepId];
|
||||
apiRequest(endpoint, @"POST", nil, apiKey);
|
||||
apiRequest(endpoint, @"POST", nil, publicKey, secretKey);
|
||||
printf("%sService sleeping: %s%s\n", [GREEN UTF8String], [sleepId UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wakeId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/wake", wakeId];
|
||||
apiRequest(endpoint, @"POST", nil, apiKey);
|
||||
apiRequest(endpoint, @"POST", nil, publicKey, secretKey);
|
||||
printf("%sService waking: %s%s\n", [GREEN UTF8String], [wakeId UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (destroyId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", destroyId];
|
||||
apiRequest(endpoint, @"DELETE", nil, apiKey);
|
||||
apiRequest(endpoint, @"DELETE", nil, publicKey, secretKey);
|
||||
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -551,7 +602,7 @@ void cmdService(NSArray* args) {
|
|||
fprintf(stderr, "Fetching bootstrap script from %s...\n", [dumpBootstrapId UTF8String]);
|
||||
NSDictionary* payload = @{@"command": @"cat /tmp/bootstrap.sh"};
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/execute", dumpBootstrapId];
|
||||
NSDictionary* result = apiRequest(endpoint, @"POST", payload, apiKey);
|
||||
NSDictionary* result = apiRequest(endpoint, @"POST", payload, publicKey, secretKey);
|
||||
|
||||
if (result[@"stdout"] && [result[@"stdout"] length] > 0) {
|
||||
NSString* bootstrap = result[@"stdout"];
|
||||
|
|
@ -610,7 +661,7 @@ void cmdService(NSArray* args) {
|
|||
if (network) payload[@"network"] = network;
|
||||
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
||||
|
||||
NSDictionary* result = apiRequest(@"/services", @"POST", payload, apiKey);
|
||||
NSDictionary* result = apiRequest(@"/services", @"POST", payload, publicKey, secretKey);
|
||||
printf("%sService created: %s%s\n", [GREEN UTF8String], [result[@"id"] UTF8String], [RESET UTF8String]);
|
||||
printf("Name: %s\n", [result[@"name"] UTF8String]);
|
||||
if (result[@"url"]) {
|
||||
|
|
|
|||
91
un.ml
91
un.ml
|
|
@ -114,11 +114,22 @@ let escape_json s =
|
|||
|
||||
(* Execute curl command *)
|
||||
let curl_post api_key endpoint json =
|
||||
let cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com%s -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '%s'"
|
||||
endpoint api_key json in
|
||||
let (public_key, secret_key) = get_api_keys () in
|
||||
let auth_headers = build_auth_headers public_key secret_key "POST" endpoint 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;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X POST https://api.unsandbox.com%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let output = read_file "/dev/stdin" in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let output = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
Sys.remove tmp_file;
|
||||
output
|
||||
|
||||
let portal_curl_post api_key endpoint json =
|
||||
|
|
@ -126,8 +137,10 @@ let portal_curl_post api_key endpoint json =
|
|||
let oc = open_out tmp_file in
|
||||
output_string oc json;
|
||||
close_out oc;
|
||||
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d @%s"
|
||||
portal_base endpoint api_key tmp_file in
|
||||
let (public_key, secret_key) = get_api_keys () in
|
||||
let auth_headers = build_auth_headers public_key secret_key "POST" endpoint json in
|
||||
let cmd = Printf.sprintf "curl -s -X POST %s%s -H 'Content-Type: application/json'%s -d @%s"
|
||||
portal_base endpoint auth_headers tmp_file in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try let line = input_line ic in read_all (acc ^ line ^ "\n")
|
||||
|
|
@ -139,8 +152,10 @@ let portal_curl_post api_key endpoint json =
|
|||
output
|
||||
|
||||
let curl_get api_key endpoint =
|
||||
let cmd = Printf.sprintf "curl -s https://api.unsandbox.com%s -H 'Authorization: Bearer %s'"
|
||||
endpoint api_key in
|
||||
let (public_key, secret_key) = get_api_keys () in
|
||||
let auth_headers = build_auth_headers public_key secret_key "GET" endpoint "" in
|
||||
let cmd = Printf.sprintf "curl -s https://api.unsandbox.com%s%s"
|
||||
endpoint auth_headers in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let rec read_all acc =
|
||||
try
|
||||
|
|
@ -153,17 +168,18 @@ let curl_get api_key endpoint =
|
|||
output
|
||||
|
||||
let curl_delete api_key endpoint =
|
||||
let cmd = Printf.sprintf "curl -s -X DELETE https://api.unsandbox.com%s -H 'Authorization: Bearer %s'"
|
||||
endpoint api_key in
|
||||
let (public_key, secret_key) = get_api_keys () in
|
||||
let auth_headers = build_auth_headers public_key secret_key "DELETE" endpoint "" in
|
||||
let cmd = Printf.sprintf "curl -s -X DELETE https://api.unsandbox.com%s%s"
|
||||
endpoint auth_headers in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let output = read_all "" where
|
||||
let rec read_all acc =
|
||||
try
|
||||
let line = input_line ic in
|
||||
read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in read_all ""
|
||||
let rec read_all acc =
|
||||
try
|
||||
let line = input_line ic in
|
||||
read_all (acc ^ line ^ "\n")
|
||||
with End_of_file -> acc
|
||||
in
|
||||
let output = read_all "" in
|
||||
let _ = Unix.close_process_in ic in
|
||||
output
|
||||
|
||||
|
|
@ -214,13 +230,46 @@ let unescape_json s =
|
|||
let s = Str.global_replace (Str.regexp "\\\\\\\\") "\\" s in
|
||||
s
|
||||
|
||||
(* Get API key *)
|
||||
let get_api_key () =
|
||||
try Sys.getenv "UNSANDBOX_API_KEY"
|
||||
with Not_found ->
|
||||
Printf.fprintf stderr "Error: UNSANDBOX_API_KEY not set\n";
|
||||
(* Get API keys *)
|
||||
let get_api_keys () =
|
||||
let public_key = try Some (Sys.getenv "UNSANDBOX_PUBLIC_KEY") with Not_found -> None in
|
||||
let secret_key = try Some (Sys.getenv "UNSANDBOX_SECRET_KEY") with Not_found -> None in
|
||||
let api_key = try Some (Sys.getenv "UNSANDBOX_API_KEY") with Not_found -> None in
|
||||
match (public_key, secret_key, api_key) with
|
||||
| (Some pk, Some sk, _) -> (pk, Some sk)
|
||||
| (_, _, Some ak) -> (ak, None)
|
||||
| _ ->
|
||||
Printf.fprintf stderr "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n";
|
||||
exit 1
|
||||
|
||||
let get_api_key () =
|
||||
let (public_key, _) = get_api_keys () in
|
||||
public_key
|
||||
|
||||
(* HMAC-SHA256 using openssl command *)
|
||||
let hmac_sha256 secret message =
|
||||
let cmd = Printf.sprintf "echo -n '%s' | openssl dgst -sha256 -hmac '%s' | awk '{print $2}'"
|
||||
(Str.global_replace (Str.regexp "'") "'\\''" message)
|
||||
(Str.global_replace (Str.regexp "'") "'\\''" secret) in
|
||||
let ic = Unix.open_process_in cmd in
|
||||
let result = input_line ic in
|
||||
let _ = Unix.close_process_in ic in
|
||||
String.trim result
|
||||
|
||||
let make_signature secret_key timestamp method_ path body =
|
||||
let message = Printf.sprintf "%s:%s:%s:%s" timestamp method_ path body in
|
||||
hmac_sha256 secret_key message
|
||||
|
||||
let build_auth_headers public_key secret_key method_ path body =
|
||||
match secret_key with
|
||||
| Some sk ->
|
||||
let timestamp = string_of_int (int_of_float (Unix.time ())) in
|
||||
let signature = make_signature sk timestamp method_ path body in
|
||||
Printf.sprintf " -H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'"
|
||||
public_key timestamp signature
|
||||
| None ->
|
||||
Printf.sprintf " -H 'Authorization: Bearer %s'" public_key
|
||||
|
||||
(* Execute command *)
|
||||
let execute_command file env_vars artifacts out_dir network vcpu =
|
||||
let api_key = get_api_key () in
|
||||
|
|
|
|||
124
un.nim
124
un.nim
|
|
@ -43,7 +43,7 @@
|
|||
# un.nim session --list
|
||||
# un.nim service --name web --ports 8080
|
||||
|
||||
import os, strutils, osproc, strformat
|
||||
import os, strutils, osproc, strformat, times
|
||||
|
||||
const
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
|
|
@ -76,10 +76,29 @@ proc escapeJson(s: string): string =
|
|||
of '\t': result.add("\\t")
|
||||
else: result.add(c)
|
||||
|
||||
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()
|
||||
|
||||
proc getTimestamp(): string =
|
||||
result = $toUnix(getTime())
|
||||
|
||||
proc buildAuthHeaders(meth: string, path: string, body: string, publicKey: string, secretKey: string): string =
|
||||
if secretKey == "":
|
||||
# Legacy mode: use public_key as bearer token
|
||||
return fmt"-H 'Authorization: Bearer {publicKey}'"
|
||||
|
||||
# HMAC mode
|
||||
let timestamp = getTimestamp()
|
||||
let message = fmt"{timestamp}:{meth}:{path}:{body}"
|
||||
let signature = computeHmac(secretKey, message)
|
||||
|
||||
return fmt"-H 'Authorization: Bearer {publicKey}' -H 'X-Timestamp: {timestamp}' -H 'X-Signature: {signature}'"
|
||||
|
||||
proc execCurl(cmd: string): string =
|
||||
result = execProcess(cmd)
|
||||
|
||||
proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network: string, vcpu: int, apiKey: string) =
|
||||
proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network: string, vcpu: int, publicKey: string, secretKey: string) =
|
||||
let lang = detectLanguage(sourceFile)
|
||||
if lang == "":
|
||||
stderr.writeLine(RED & "Error: Cannot detect language" & RESET)
|
||||
|
|
@ -102,17 +121,21 @@ proc cmdExecute(sourceFile: string, envs: seq[string], artifacts: bool, network:
|
|||
if vcpu > 0: json.add(fmt""","vcpu":{vcpu}""")
|
||||
json.add("}")
|
||||
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'"""
|
||||
let authHeaders = buildAuthHeaders("POST", "/execute", json, publicKey, secretKey)
|
||||
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, apiKey: string) =
|
||||
proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, screen: bool, publicKey: string, secretKey: string) =
|
||||
if list:
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let authHeaders = buildAuthHeaders("GET", "/sessions", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/sessions' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if kill != "":
|
||||
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/sessions/{kill}' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/sessions/{kill}"
|
||||
let authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/sessions/{kill}' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Session terminated: " & kill & RESET
|
||||
return
|
||||
|
|
@ -125,51 +148,67 @@ proc cmdSession(list: bool, kill, shell, network: string, vcpu: int, tmux, scree
|
|||
json.add("}")
|
||||
|
||||
echo YELLOW & "Creating session..." & RESET
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'"""
|
||||
let authHeaders = buildAuthHeaders("POST", "/sessions", json, publicKey, secretKey)
|
||||
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, apiKey: string) =
|
||||
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) =
|
||||
if list:
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let authHeaders = buildAuthHeaders("GET", "/services", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if info != "":
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{info}' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/services/{info}"
|
||||
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{info}' {authHeaders}"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
if logs != "":
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{logs}/logs' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/services/{logs}/logs"
|
||||
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{logs}/logs' {authHeaders}"""
|
||||
stdout.write(execCurl(cmd))
|
||||
return
|
||||
|
||||
if tail != "":
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{tail}/logs?lines=9000' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/services/{tail}/logs"
|
||||
let authHeaders = buildAuthHeaders("GET", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X GET '{API_BASE}/services/{tail}/logs?lines=9000' {authHeaders}"""
|
||||
stdout.write(execCurl(cmd))
|
||||
return
|
||||
|
||||
if sleep != "":
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{sleep}/sleep' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/services/{sleep}/sleep"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{sleep}/sleep' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Service sleeping: " & sleep & RESET
|
||||
return
|
||||
|
||||
if wake != "":
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{wake}/wake' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/services/{wake}/wake"
|
||||
let authHeaders = buildAuthHeaders("POST", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{wake}/wake' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Service waking: " & wake & RESET
|
||||
return
|
||||
|
||||
if destroy != "":
|
||||
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/services/{destroy}' -H 'Authorization: Bearer {apiKey}'"""
|
||||
let path = fmt"/services/{destroy}"
|
||||
let authHeaders = buildAuthHeaders("DELETE", path, "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X DELETE '{API_BASE}/services/{destroy}' {authHeaders}"""
|
||||
discard execCurl(cmd)
|
||||
echo GREEN & "Service destroyed: " & destroy & RESET
|
||||
return
|
||||
|
||||
if execute != "":
|
||||
let json = fmt"""{"command":"{escapeJson(command)}"}"""
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'"""
|
||||
let path = fmt"/services/{execute}/execute"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{execute}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let result = execCurl(cmd)
|
||||
|
||||
# Simple parsing for stdout/stderr
|
||||
|
|
@ -202,7 +241,10 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
|
|||
|
||||
if dumpBootstrap != "":
|
||||
stderr.writeLine("Fetching bootstrap script from " & dumpBootstrap & "...")
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{dumpBootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{{"command":"cat /tmp/bootstrap.sh"}}'"""
|
||||
let json = """{"command":"cat /tmp/bootstrap.sh"}"""
|
||||
let path = fmt"/services/{dumpBootstrap}/execute"
|
||||
let authHeaders = buildAuthHeaders("POST", path, json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services/{dumpBootstrap}/execute' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
let result = execCurl(cmd)
|
||||
|
||||
let stdoutStart = result.find("\"stdout\":\"")
|
||||
|
|
@ -252,15 +294,17 @@ proc cmdService(name, ports, bootstrap, serviceType: string, list: bool, info, l
|
|||
json.add("}")
|
||||
|
||||
echo YELLOW & "Creating service..." & RESET
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer {apiKey}' -d '{json}'"""
|
||||
let authHeaders = buildAuthHeaders("POST", "/services", json, publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{API_BASE}/services' -H 'Content-Type: application/json' {authHeaders} -d '{json}'"""
|
||||
echo execCurl(cmd)
|
||||
return
|
||||
|
||||
stderr.writeLine(RED & "Error: Specify --name to create a service" & RESET)
|
||||
quit(1)
|
||||
|
||||
proc cmdKey(extend: bool, apiKey: string) =
|
||||
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' -H 'Authorization: Bearer {apiKey}'"""
|
||||
proc cmdKey(extend: bool, publicKey: string, secretKey: string) =
|
||||
let authHeaders = buildAuthHeaders("POST", "/keys/validate", "", publicKey, secretKey)
|
||||
let cmd = fmt"""curl -s -X POST '{PORTAL_BASE}/keys/validate' {authHeaders}"""
|
||||
let response = execCurl(cmd)
|
||||
|
||||
# Parse JSON response manually (simple approach)
|
||||
|
|
@ -271,10 +315,10 @@ proc cmdKey(extend: bool, apiKey: string) =
|
|||
let pkStart = response.find("\"public_key\":\"") + 14
|
||||
let pkEnd = response.find("\"", pkStart)
|
||||
if pkEnd > pkStart:
|
||||
let publicKey = response[pkStart..<pkEnd]
|
||||
echo "Public Key: " & publicKey
|
||||
let pubKey = response[pkStart..<pkEnd]
|
||||
echo "Public Key: " & pubKey
|
||||
if extend:
|
||||
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}"
|
||||
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={pubKey}"
|
||||
echo BLUE & "Opening browser to extend key..." & RESET
|
||||
discard execProcess(fmt"xdg-open '{extendUrl}'")
|
||||
|
||||
|
|
@ -296,13 +340,13 @@ proc cmdKey(extend: bool, apiKey: string) =
|
|||
echo RED & "Expired" & RESET
|
||||
|
||||
# Extract and display key info
|
||||
var publicKey = ""
|
||||
var pubKey = ""
|
||||
if response.contains("\"public_key\":"):
|
||||
let pkStart = response.find("\"public_key\":\"") + 14
|
||||
let pkEnd = response.find("\"", pkStart)
|
||||
if pkEnd > pkStart:
|
||||
publicKey = response[pkStart..<pkEnd]
|
||||
echo "Public Key: " & publicKey
|
||||
pubKey = response[pkStart..<pkEnd]
|
||||
echo "Public Key: " & pubKey
|
||||
|
||||
# Extract tier
|
||||
if response.contains("\"tier\":"):
|
||||
|
|
@ -321,8 +365,8 @@ proc cmdKey(extend: bool, apiKey: string) =
|
|||
echo ""
|
||||
echo YELLOW & "To renew: Visit " & PORTAL_BASE & "/keys/extend" & RESET
|
||||
|
||||
if extend and publicKey != "":
|
||||
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={publicKey}"
|
||||
if extend and pubKey != "":
|
||||
let extendUrl = fmt"{PORTAL_BASE}/keys/extend?pk={pubKey}"
|
||||
echo BLUE & "Opening browser to extend key..." & RESET
|
||||
discard execProcess(fmt"xdg-open '{extendUrl}'")
|
||||
|
||||
|
|
@ -335,7 +379,13 @@ proc cmdKey(extend: bool, apiKey: string) =
|
|||
echo "Error: " & response[errStart..<errEnd]
|
||||
|
||||
proc main() =
|
||||
var apiKey = getEnv("UNSANDBOX_API_KEY", "")
|
||||
var publicKey = getEnv("UNSANDBOX_PUBLIC_KEY", "")
|
||||
var secretKey = getEnv("UNSANDBOX_SECRET_KEY", "")
|
||||
|
||||
# Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if publicKey == "":
|
||||
publicKey = getEnv("UNSANDBOX_API_KEY", "")
|
||||
|
||||
let args = commandLineParams()
|
||||
|
||||
if args.len < 1:
|
||||
|
|
@ -351,9 +401,9 @@ proc main() =
|
|||
while i < args.len:
|
||||
case args[i]
|
||||
of "--extend": extend = true
|
||||
of "-k": apiKey = args[i+1]; inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
inc i
|
||||
cmdKey(extend, apiKey)
|
||||
cmdKey(extend, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "session":
|
||||
|
|
@ -371,9 +421,9 @@ proc main() =
|
|||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||
of "--tmux": tmux = true
|
||||
of "--screen": screen = true
|
||||
of "-k": apiKey = args[i+1]; inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
inc i
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, apiKey)
|
||||
cmdSession(list, kill, shell, network, vcpu, tmux, screen, publicKey, secretKey)
|
||||
return
|
||||
|
||||
if args[0] == "service":
|
||||
|
|
@ -401,9 +451,9 @@ proc main() =
|
|||
of "--dump-file": dumpFile = args[i+1]; inc i
|
||||
of "-n": network = args[i+1]; inc i
|
||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||
of "-k": apiKey = args[i+1]; inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
inc i
|
||||
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, apiKey)
|
||||
cmdService(name, ports, bootstrap, serviceType, list, info, logs, tail, sleep, wake, destroy, execute, command, dumpBootstrap, dumpFile, network, vcpu, publicKey, secretKey)
|
||||
return
|
||||
|
||||
# Execute mode
|
||||
|
|
@ -418,7 +468,7 @@ proc main() =
|
|||
of "-a": artifacts = true
|
||||
of "-n": network = args[i+1]; inc i
|
||||
of "-v": vcpu = parseInt(args[i+1]); inc i
|
||||
of "-k": apiKey = args[i+1]; inc i
|
||||
of "-k": publicKey = args[i+1]; inc i
|
||||
else:
|
||||
if not args[i].startsWith("-"):
|
||||
sourceFile = args[i]
|
||||
|
|
@ -428,7 +478,7 @@ proc main() =
|
|||
stderr.writeLine(RED & "Error: No source file specified" & RESET)
|
||||
quit(1)
|
||||
|
||||
cmdExecute(sourceFile, envs, artifacts, network, vcpu, apiKey)
|
||||
cmdExecute(sourceFile, envs, artifacts, network, vcpu, publicKey, secretKey)
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
|
|
|
|||
103
un.php
103
un.php
|
|
@ -76,13 +76,23 @@ const EXT_MAP = [
|
|||
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
|
||||
];
|
||||
|
||||
function get_api_key($args_key = null) {
|
||||
$key = $args_key ?: getenv('UNSANDBOX_API_KEY');
|
||||
if (!$key) {
|
||||
fwrite(STDERR, RED . "Error: UNSANDBOX_API_KEY not set" . RESET . "\n");
|
||||
exit(1);
|
||||
function get_api_keys($args_key = null) {
|
||||
$public_key = getenv('UNSANDBOX_PUBLIC_KEY');
|
||||
$secret_key = getenv('UNSANDBOX_SECRET_KEY');
|
||||
|
||||
if (!$public_key || !$secret_key) {
|
||||
$old_key = $args_key ?: getenv('UNSANDBOX_API_KEY');
|
||||
if ($old_key) {
|
||||
$public_key = $old_key;
|
||||
$secret_key = $old_key;
|
||||
} else {
|
||||
fwrite(STDERR, RED . "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set" . RESET . "\n");
|
||||
fwrite(STDERR, RED . " (or legacy UNSANDBOX_API_KEY for backwards compatibility)" . RESET . "\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
return $key;
|
||||
|
||||
return ['public_key' => $public_key, 'secret_key' => $secret_key];
|
||||
}
|
||||
|
||||
function detect_language($filename) {
|
||||
|
|
@ -109,12 +119,23 @@ function detect_language($filename) {
|
|||
return $lang;
|
||||
}
|
||||
|
||||
function api_request($endpoint, $method = 'GET', $data = null, $api_key = null) {
|
||||
function api_request($endpoint, $method = 'GET', $data = null, $keys = null) {
|
||||
$url = API_BASE . $endpoint;
|
||||
$ch = curl_init($url);
|
||||
|
||||
$timestamp = (string)time();
|
||||
$body = $data ? json_encode($data) : '';
|
||||
|
||||
// Parse URL to get path and query
|
||||
$parsed_url = parse_url($url);
|
||||
$path = $parsed_url['path'] . (isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '');
|
||||
$message = "$timestamp:$method:$path:$body";
|
||||
$signature = hash_hmac('sha256', $message, $keys['secret_key']);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $api_key,
|
||||
'Authorization: Bearer ' . $keys['public_key'],
|
||||
'X-Timestamp: ' . $timestamp,
|
||||
'X-Signature: ' . $signature,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
|
|
@ -126,7 +147,7 @@ function api_request($endpoint, $method = 'GET', $data = null, $api_key = null)
|
|||
]);
|
||||
|
||||
if ($data) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
|
@ -149,7 +170,7 @@ function api_request($endpoint, $method = 'GET', $data = null, $api_key = null)
|
|||
}
|
||||
|
||||
function cmd_execute($options) {
|
||||
$api_key = get_api_key($options['api_key']);
|
||||
$keys = get_api_keys($options['api_key']);
|
||||
|
||||
if (!file_exists($options['source_file'])) {
|
||||
fwrite(STDERR, RED . "Error: File not found: {$options['source_file']}" . RESET . "\n");
|
||||
|
|
@ -193,7 +214,7 @@ function cmd_execute($options) {
|
|||
if ($options['network']) $payload['network'] = $options['network'];
|
||||
if ($options['vcpu']) $payload['vcpu'] = $options['vcpu'];
|
||||
|
||||
$result = api_request('/execute', 'POST', $payload, $api_key);
|
||||
$result = api_request('/execute', 'POST', $payload, $keys);
|
||||
|
||||
if (!empty($result['stdout'])) {
|
||||
echo BLUE . $result['stdout'] . RESET;
|
||||
|
|
@ -221,10 +242,10 @@ function cmd_execute($options) {
|
|||
}
|
||||
|
||||
function cmd_session($options) {
|
||||
$api_key = get_api_key($options['api_key']);
|
||||
$keys = get_api_keys($options['api_key']);
|
||||
|
||||
if ($options['list']) {
|
||||
$result = api_request('/sessions', 'GET', null, $api_key);
|
||||
$result = api_request('/sessions', 'GET', null, $keys);
|
||||
$sessions = $result['sessions'] ?? [];
|
||||
if (empty($sessions)) {
|
||||
echo "No active sessions\n";
|
||||
|
|
@ -240,7 +261,7 @@ function cmd_session($options) {
|
|||
}
|
||||
|
||||
if ($options['kill']) {
|
||||
api_request("/sessions/{$options['kill']}", 'DELETE', null, $api_key);
|
||||
api_request("/sessions/{$options['kill']}", 'DELETE', null, $keys);
|
||||
echo GREEN . "Session terminated: {$options['kill']}" . RESET . "\n";
|
||||
return;
|
||||
}
|
||||
|
|
@ -259,17 +280,27 @@ function cmd_session($options) {
|
|||
if ($options['audit']) $payload['audit'] = true;
|
||||
|
||||
echo YELLOW . "Creating session..." . RESET . "\n";
|
||||
$result = api_request('/sessions', 'POST', $payload, $api_key);
|
||||
$result = api_request('/sessions', 'POST', $payload, $keys);
|
||||
echo GREEN . "Session created: " . ($result['id'] ?? 'N/A') . RESET . "\n";
|
||||
echo YELLOW . "(Interactive sessions require WebSocket - use un2 for full support)" . RESET . "\n";
|
||||
}
|
||||
|
||||
function validate_key($api_key) {
|
||||
function validate_key($keys) {
|
||||
$url = PORTAL_BASE . '/keys/validate';
|
||||
$ch = curl_init($url);
|
||||
|
||||
$timestamp = (string)time();
|
||||
$body = '';
|
||||
|
||||
$parsed_url = parse_url($url);
|
||||
$path = $parsed_url['path'];
|
||||
$message = "$timestamp:POST:$path:$body";
|
||||
$signature = hash_hmac('sha256', $message, $keys['secret_key']);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $api_key,
|
||||
'Authorization: Bearer ' . $keys['public_key'],
|
||||
'X-Timestamp: ' . $timestamp,
|
||||
'X-Signature: ' . $signature,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
|
|
@ -322,15 +353,25 @@ function validate_key($api_key) {
|
|||
}
|
||||
|
||||
function cmd_key($options) {
|
||||
$api_key = get_api_key($options['api_key']);
|
||||
$keys = get_api_keys($options['api_key']);
|
||||
|
||||
if ($options['extend']) {
|
||||
// First validate to get public_key
|
||||
$url = PORTAL_BASE . '/keys/validate';
|
||||
$ch = curl_init($url);
|
||||
|
||||
$timestamp = (string)time();
|
||||
$body = '';
|
||||
|
||||
$parsed_url = parse_url($url);
|
||||
$path = $parsed_url['path'];
|
||||
$message = "$timestamp:POST:$path:$body";
|
||||
$signature = hash_hmac('sha256', $message, $keys['secret_key']);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $api_key,
|
||||
'Authorization: Bearer ' . $keys['public_key'],
|
||||
'X-Timestamp: ' . $timestamp,
|
||||
'X-Signature: ' . $signature,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
|
|
@ -367,15 +408,15 @@ function cmd_key($options) {
|
|||
echo "$extend_url\n";
|
||||
}
|
||||
} else {
|
||||
validate_key($api_key);
|
||||
validate_key($keys);
|
||||
}
|
||||
}
|
||||
|
||||
function cmd_service($options) {
|
||||
$api_key = get_api_key($options['api_key']);
|
||||
$keys = get_api_keys($options['api_key']);
|
||||
|
||||
if ($options['list']) {
|
||||
$result = api_request('/services', 'GET', null, $api_key);
|
||||
$result = api_request('/services', 'GET', null, $keys);
|
||||
$services = $result['services'] ?? [];
|
||||
if (empty($services)) {
|
||||
echo "No services\n";
|
||||
|
|
@ -393,44 +434,44 @@ function cmd_service($options) {
|
|||
}
|
||||
|
||||
if ($options['info']) {
|
||||
$result = api_request("/services/{$options['info']}", 'GET', null, $api_key);
|
||||
$result = api_request("/services/{$options['info']}", 'GET', null, $keys);
|
||||
echo json_encode($result, JSON_PRETTY_PRINT) . "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options['logs']) {
|
||||
$result = api_request("/services/{$options['logs']}/logs", 'GET', null, $api_key);
|
||||
$result = api_request("/services/{$options['logs']}/logs", 'GET', null, $keys);
|
||||
echo $result['logs'] ?? '';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options['tail']) {
|
||||
$result = api_request("/services/{$options['tail']}/logs?lines=9000", 'GET', null, $api_key);
|
||||
$result = api_request("/services/{$options['tail']}/logs?lines=9000", 'GET', null, $keys);
|
||||
echo $result['logs'] ?? '';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options['sleep']) {
|
||||
api_request("/services/{$options['sleep']}/sleep", 'POST', null, $api_key);
|
||||
api_request("/services/{$options['sleep']}/sleep", 'POST', null, $keys);
|
||||
echo GREEN . "Service sleeping: {$options['sleep']}" . RESET . "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options['wake']) {
|
||||
api_request("/services/{$options['wake']}/wake", 'POST', null, $api_key);
|
||||
api_request("/services/{$options['wake']}/wake", 'POST', null, $keys);
|
||||
echo GREEN . "Service waking: {$options['wake']}" . RESET . "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options['destroy']) {
|
||||
api_request("/services/{$options['destroy']}", 'DELETE', null, $api_key);
|
||||
api_request("/services/{$options['destroy']}", 'DELETE', null, $keys);
|
||||
echo GREEN . "Service destroyed: {$options['destroy']}" . RESET . "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options['execute']) {
|
||||
$payload = ['command' => $options['command']];
|
||||
$result = api_request("/services/{$options['execute']}/execute", 'POST', $payload, $api_key);
|
||||
$result = api_request("/services/{$options['execute']}/execute", 'POST', $payload, $keys);
|
||||
if (!empty($result['stdout'])) echo BLUE . $result['stdout'] . RESET;
|
||||
if (!empty($result['stderr'])) fwrite(STDERR, RED . $result['stderr'] . RESET);
|
||||
return;
|
||||
|
|
@ -439,7 +480,7 @@ function cmd_service($options) {
|
|||
if ($options['dump_bootstrap']) {
|
||||
fwrite(STDERR, "Fetching bootstrap script from {$options['dump_bootstrap']}...\n");
|
||||
$payload = ['command' => 'cat /tmp/bootstrap.sh'];
|
||||
$result = api_request("/services/{$options['dump_bootstrap']}/execute", 'POST', $payload, $api_key);
|
||||
$result = api_request("/services/{$options['dump_bootstrap']}/execute", 'POST', $payload, $keys);
|
||||
|
||||
if (!empty($result['stdout'])) {
|
||||
$bootstrap = $result['stdout'];
|
||||
|
|
@ -483,7 +524,7 @@ function cmd_service($options) {
|
|||
if ($options['network']) $payload['network'] = $options['network'];
|
||||
if ($options['vcpu']) $payload['vcpu'] = $options['vcpu'];
|
||||
|
||||
$result = api_request('/services', 'POST', $payload, $api_key);
|
||||
$result = api_request('/services', 'POST', $payload, $keys);
|
||||
echo GREEN . "Service created: " . ($result['id'] ?? 'N/A') . RESET . "\n";
|
||||
echo "Name: " . ($result['name'] ?? 'N/A') . "\n";
|
||||
if (!empty($result['url'])) echo "URL: {$result['url']}\n";
|
||||
|
|
|
|||
85
un.pl
85
un.pl
|
|
@ -57,6 +57,7 @@ use LWP::UserAgent;
|
|||
use HTTP::Request;
|
||||
use MIME::Base64;
|
||||
use File::Path qw(make_path);
|
||||
use Digest::SHA qw(hmac_sha256_hex);
|
||||
|
||||
my $API_BASE = 'https://api.unsandbox.com';
|
||||
my $PORTAL_BASE = 'https://unsandbox.com';
|
||||
|
|
@ -84,12 +85,20 @@ my %EXT_MAP = (
|
|||
|
||||
sub get_api_key {
|
||||
my ($args_key) = @_;
|
||||
my $key = $args_key || $ENV{'UNSANDBOX_API_KEY'};
|
||||
unless ($key) {
|
||||
print STDERR "${RED}Error: UNSANDBOX_API_KEY not set${RESET}\n";
|
||||
my $public_key = $ENV{'UNSANDBOX_PUBLIC_KEY'} || '';
|
||||
my $secret_key = $ENV{'UNSANDBOX_SECRET_KEY'} || '';
|
||||
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if (!$public_key && $ENV{'UNSANDBOX_API_KEY'}) {
|
||||
$public_key = $ENV{'UNSANDBOX_API_KEY'};
|
||||
$secret_key = '';
|
||||
}
|
||||
|
||||
unless ($public_key) {
|
||||
print STDERR "${RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${RESET}\n";
|
||||
exit 1;
|
||||
}
|
||||
return $key;
|
||||
return ($public_key, $secret_key);
|
||||
}
|
||||
|
||||
sub detect_language {
|
||||
|
|
@ -117,17 +126,28 @@ sub detect_language {
|
|||
}
|
||||
|
||||
sub api_request {
|
||||
my ($endpoint, $method, $data, $api_key) = @_;
|
||||
my ($endpoint, $method, $data, $public_key, $secret_key) = @_;
|
||||
$method //= 'GET';
|
||||
|
||||
my $url = "$API_BASE$endpoint";
|
||||
my $ua = LWP::UserAgent->new(timeout => 300);
|
||||
my $request = HTTP::Request->new($method => $url);
|
||||
$request->header('Authorization' => "Bearer $api_key");
|
||||
$request->header('Authorization' => "Bearer $public_key");
|
||||
$request->header('Content-Type' => 'application/json');
|
||||
|
||||
my $body = '';
|
||||
if ($data) {
|
||||
$request->content(encode_json($data));
|
||||
$body = encode_json($data);
|
||||
$request->content($body);
|
||||
}
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if ($secret_key) {
|
||||
my $timestamp = time();
|
||||
my $sig_input = "${timestamp}:${method}:${endpoint}:${body}";
|
||||
my $signature = hmac_sha256_hex($sig_input, $secret_key);
|
||||
$request->header('X-Timestamp' => $timestamp);
|
||||
$request->header('X-Signature' => $signature);
|
||||
}
|
||||
|
||||
my $response = $ua->request($request);
|
||||
|
|
@ -142,7 +162,7 @@ sub api_request {
|
|||
|
||||
sub cmd_execute {
|
||||
my ($options) = @_;
|
||||
my $api_key = get_api_key($options->{api_key});
|
||||
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
||||
|
||||
unless (-e $options->{source_file}) {
|
||||
print STDERR "${RED}Error: File not found: $options->{source_file}${RESET}\n";
|
||||
|
|
@ -190,7 +210,7 @@ sub cmd_execute {
|
|||
$payload->{network} = $options->{network} if $options->{network};
|
||||
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
|
||||
|
||||
my $result = api_request('/execute', 'POST', $payload, $api_key);
|
||||
my $result = api_request('/execute', 'POST', $payload, $public_key, $secret_key);
|
||||
|
||||
print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout};
|
||||
print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
|
||||
|
|
@ -215,10 +235,10 @@ sub cmd_execute {
|
|||
|
||||
sub cmd_session {
|
||||
my ($options) = @_;
|
||||
my $api_key = get_api_key($options->{api_key});
|
||||
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
||||
|
||||
if ($options->{list}) {
|
||||
my $result = api_request('/sessions', 'GET', undef, $api_key);
|
||||
my $result = api_request('/sessions', 'GET', undef, $public_key, $secret_key);
|
||||
my $sessions = $result->{sessions} || [];
|
||||
if (@$sessions == 0) {
|
||||
print "No active sessions\n";
|
||||
|
|
@ -234,7 +254,7 @@ sub cmd_session {
|
|||
}
|
||||
|
||||
if ($options->{kill}) {
|
||||
api_request("/sessions/$options->{kill}", 'DELETE', undef, $api_key);
|
||||
api_request("/sessions/$options->{kill}", 'DELETE', undef, $public_key, $secret_key);
|
||||
print "${GREEN}Session terminated: $options->{kill}${RESET}\n";
|
||||
return;
|
||||
}
|
||||
|
|
@ -253,17 +273,17 @@ sub cmd_session {
|
|||
$payload->{audit} = JSON::PP::true if $options->{audit};
|
||||
|
||||
print "${YELLOW}Creating session...${RESET}\n";
|
||||
my $result = api_request('/sessions', 'POST', $payload, $api_key);
|
||||
my $result = api_request('/sessions', 'POST', $payload, $public_key, $secret_key);
|
||||
print "${GREEN}Session created: ", ($result->{id} // 'N/A'), "${RESET}\n";
|
||||
print "${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}\n";
|
||||
}
|
||||
|
||||
sub cmd_service {
|
||||
my ($options) = @_;
|
||||
my $api_key = get_api_key($options->{api_key});
|
||||
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
||||
|
||||
if ($options->{list}) {
|
||||
my $result = api_request('/services', 'GET', undef, $api_key);
|
||||
my $result = api_request('/services', 'GET', undef, $public_key, $secret_key);
|
||||
my $services = $result->{services} || [];
|
||||
if (@$services == 0) {
|
||||
print "No services\n";
|
||||
|
|
@ -281,45 +301,45 @@ sub cmd_service {
|
|||
}
|
||||
|
||||
if ($options->{info}) {
|
||||
my $result = api_request("/services/$options->{info}", 'GET', undef, $api_key);
|
||||
my $result = api_request("/services/$options->{info}", 'GET', undef, $public_key, $secret_key);
|
||||
print encode_json($result);
|
||||
print "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options->{logs}) {
|
||||
my $result = api_request("/services/$options->{logs}/logs", 'GET', undef, $api_key);
|
||||
my $result = api_request("/services/$options->{logs}/logs", 'GET', undef, $public_key, $secret_key);
|
||||
print $result->{logs} // '';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options->{tail}) {
|
||||
my $result = api_request("/services/$options->{tail}/logs?lines=9000", 'GET', undef, $api_key);
|
||||
my $result = api_request("/services/$options->{tail}/logs?lines=9000", 'GET', undef, $public_key, $secret_key);
|
||||
print $result->{logs} // '';
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options->{sleep}) {
|
||||
api_request("/services/$options->{sleep}/sleep", 'POST', undef, $api_key);
|
||||
api_request("/services/$options->{sleep}/sleep", 'POST', undef, $public_key, $secret_key);
|
||||
print "${GREEN}Service sleeping: $options->{sleep}${RESET}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options->{wake}) {
|
||||
api_request("/services/$options->{wake}/wake", 'POST', undef, $api_key);
|
||||
api_request("/services/$options->{wake}/wake", 'POST', undef, $public_key, $secret_key);
|
||||
print "${GREEN}Service waking: $options->{wake}${RESET}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options->{destroy}) {
|
||||
api_request("/services/$options->{destroy}", 'DELETE', undef, $api_key);
|
||||
api_request("/services/$options->{destroy}", 'DELETE', undef, $public_key, $secret_key);
|
||||
print "${GREEN}Service destroyed: $options->{destroy}${RESET}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($options->{execute}) {
|
||||
my $payload = { command => $options->{command} };
|
||||
my $result = api_request("/services/$options->{execute}/execute", 'POST', $payload, $api_key);
|
||||
my $result = api_request("/services/$options->{execute}/execute", 'POST', $payload, $public_key, $secret_key);
|
||||
print "${BLUE}$result->{stdout}${RESET}" if $result->{stdout};
|
||||
print STDERR "${RED}$result->{stderr}${RESET}" if $result->{stderr};
|
||||
return;
|
||||
|
|
@ -328,7 +348,7 @@ sub cmd_service {
|
|||
if ($options->{dump_bootstrap}) {
|
||||
print STDERR "Fetching bootstrap script from $options->{dump_bootstrap}...\n";
|
||||
my $payload = { command => 'cat /tmp/bootstrap.sh' };
|
||||
my $result = api_request("/services/$options->{dump_bootstrap}/execute", 'POST', $payload, $api_key);
|
||||
my $result = api_request("/services/$options->{dump_bootstrap}/execute", 'POST', $payload, $public_key, $secret_key);
|
||||
|
||||
if ($result->{stdout}) {
|
||||
my $bootstrap = $result->{stdout};
|
||||
|
|
@ -379,7 +399,7 @@ sub cmd_service {
|
|||
$payload->{network} = $options->{network} if $options->{network};
|
||||
$payload->{vcpu} = $options->{vcpu} if $options->{vcpu};
|
||||
|
||||
my $result = api_request('/services', 'POST', $payload, $api_key);
|
||||
my $result = api_request('/services', 'POST', $payload, $public_key, $secret_key);
|
||||
print "${GREEN}Service created: ", ($result->{id} // 'N/A'), "${RESET}\n";
|
||||
print "Name: ", ($result->{name} // 'N/A'), "\n";
|
||||
print "URL: $result->{url}\n" if $result->{url};
|
||||
|
|
@ -405,15 +425,24 @@ sub open_browser {
|
|||
}
|
||||
|
||||
sub validate_key {
|
||||
my ($api_key, $should_extend) = @_;
|
||||
my ($public_key, $secret_key, $should_extend) = @_;
|
||||
|
||||
# Call /keys/validate endpoint
|
||||
my $url = "$PORTAL_BASE/keys/validate";
|
||||
my $ua = LWP::UserAgent->new(timeout => 30);
|
||||
my $request = HTTP::Request->new('POST' => $url);
|
||||
$request->header('Authorization' => "Bearer $api_key");
|
||||
$request->header('Authorization' => "Bearer $public_key");
|
||||
$request->header('Content-Type' => 'application/json');
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if ($secret_key) {
|
||||
my $timestamp = time();
|
||||
my $sig_input = "${timestamp}:POST:/keys/validate:";
|
||||
my $signature = hmac_sha256_hex($sig_input, $secret_key);
|
||||
$request->header('X-Timestamp' => $timestamp);
|
||||
$request->header('X-Signature' => $signature);
|
||||
}
|
||||
|
||||
my $response = $ua->request($request);
|
||||
my $result = decode_json($response->content);
|
||||
|
||||
|
|
@ -455,8 +484,8 @@ sub validate_key {
|
|||
|
||||
sub cmd_key {
|
||||
my ($options) = @_;
|
||||
my $api_key = get_api_key($options->{api_key});
|
||||
validate_key($api_key, $options->{extend});
|
||||
my ($public_key, $secret_key) = get_api_key($options->{api_key});
|
||||
validate_key($public_key, $secret_key, $options->{extend});
|
||||
}
|
||||
|
||||
sub main {
|
||||
|
|
|
|||
125
un.pro
125
un.pro
|
|
@ -79,15 +79,32 @@ read_file_content(Filename, Content) :-
|
|||
read_string(Stream, _, Content),
|
||||
close(Stream).
|
||||
|
||||
% Get API key from environment
|
||||
get_api_key(ApiKey) :-
|
||||
( getenv('UNSANDBOX_API_KEY', ApiKey),
|
||||
ApiKey \= ''
|
||||
% Get API keys from environment (HMAC or legacy)
|
||||
get_public_key(PublicKey) :-
|
||||
( getenv('UNSANDBOX_PUBLIC_KEY', PublicKey),
|
||||
PublicKey \= ''
|
||||
-> true
|
||||
; write(user_error, 'Error: UNSANDBOX_API_KEY environment variable not set\n'),
|
||||
; getenv('UNSANDBOX_API_KEY', PublicKey),
|
||||
PublicKey \= ''
|
||||
-> true
|
||||
; write(user_error, 'Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set\n'),
|
||||
halt(1)
|
||||
).
|
||||
|
||||
get_secret_key(SecretKey) :-
|
||||
( getenv('UNSANDBOX_SECRET_KEY', SecretKey),
|
||||
SecretKey \= ''
|
||||
-> true
|
||||
; getenv('UNSANDBOX_API_KEY', SecretKey),
|
||||
SecretKey \= ''
|
||||
-> true
|
||||
; SecretKey = ''
|
||||
).
|
||||
|
||||
% Get API key (legacy compatibility)
|
||||
get_api_key(ApiKey) :-
|
||||
get_public_key(ApiKey).
|
||||
|
||||
% Execute command using curl
|
||||
execute_file(Filename) :-
|
||||
% Check file exists
|
||||
|
|
@ -105,97 +122,108 @@ execute_file(Filename) :-
|
|||
halt(1)
|
||||
),
|
||||
|
||||
% Get API key
|
||||
get_api_key(ApiKey),
|
||||
% Get API keys
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
|
||||
% Build and execute curl command
|
||||
% Build and execute curl command with HMAC
|
||||
format(atom(Cmd),
|
||||
'curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" --data-binary @- -o /tmp/unsandbox_resp.json < <(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json',
|
||||
[ApiKey, Language, Filename]),
|
||||
'BODY=$(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_resp.json; jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json',
|
||||
[Language, Filename, SecretKey, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Session list
|
||||
session_list :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"',
|
||||
[ApiKey]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/sessions:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"',
|
||||
[SecretKey, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Session kill
|
||||
session_kill(SessionId) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X DELETE https://api.unsandbox.com/sessions/~w -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mSession terminated: ~w\\x1b[0m"',
|
||||
[SessionId, ApiKey, SessionId]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:DELETE:/sessions/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X DELETE https://api.unsandbox.com/sessions/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mSession terminated: ~w\\x1b[0m"',
|
||||
[SessionId, SecretKey, SessionId, PublicKey, SessionId]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service list
|
||||
service_list :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"',
|
||||
[ApiKey]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"',
|
||||
[SecretKey, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service info
|
||||
service_info(ServiceId) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X GET https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" | jq .',
|
||||
[ServiceId, ApiKey]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq .',
|
||||
[ServiceId, SecretKey, ServiceId, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service logs
|
||||
service_logs(ServiceId) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X GET https://api.unsandbox.com/services/~w/logs -H "Authorization: Bearer ~w" | jq -r ".logs"',
|
||||
[ServiceId, ApiKey]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services/~w/logs:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services/~w/logs -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r ".logs"',
|
||||
[ServiceId, SecretKey, ServiceId, PublicKey]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service sleep
|
||||
service_sleep(ServiceId) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X POST https://api.unsandbox.com/services/~w/sleep -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mService sleeping: ~w\\x1b[0m"',
|
||||
[ServiceId, ApiKey, ServiceId]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/sleep:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services/~w/sleep -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mService sleeping: ~w\\x1b[0m"',
|
||||
[ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service wake
|
||||
service_wake(ServiceId) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X POST https://api.unsandbox.com/services/~w/wake -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mService waking: ~w\\x1b[0m"',
|
||||
[ServiceId, ApiKey, ServiceId]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/wake:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST https://api.unsandbox.com/services/~w/wake -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mService waking: ~w\\x1b[0m"',
|
||||
[ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service destroy
|
||||
service_destroy(ServiceId) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
format(atom(Cmd),
|
||||
'curl -s -X DELETE https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" >/dev/null && echo -e "\\x1b[32mService destroyed: ~w\\x1b[0m"',
|
||||
[ServiceId, ApiKey, ServiceId]),
|
||||
'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:DELETE:/services/~w:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X DELETE https://api.unsandbox.com/services/~w -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" >/dev/null && echo -e "\\x1b[32mService destroyed: ~w\\x1b[0m"',
|
||||
[ServiceId, SecretKey, ServiceId, PublicKey, ServiceId]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service dump bootstrap
|
||||
service_dump_bootstrap(ServiceId, DumpFile) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
( DumpFile = ''
|
||||
-> % No file specified, print to stdout
|
||||
format(atom(Cmd),
|
||||
'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
|
||||
[ServiceId, ServiceId, ApiKey])
|
||||
'echo "Fetching bootstrap script from ~w..." >&2; BODY=\'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
|
||||
[ServiceId, ServiceId, SecretKey, ServiceId, PublicKey])
|
||||
; % File specified, save to file
|
||||
format(atom(Cmd),
|
||||
'echo "Fetching bootstrap script from ~w..." >&2; RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT" > "~w" && chmod 755 "~w" && echo "Bootstrap saved to ~w"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
|
||||
[ServiceId, ServiceId, ApiKey, DumpFile, DumpFile, DumpFile])
|
||||
'echo "Fetching bootstrap script from ~w..." >&2; BODY=\'\'\'\'{"command":"cat /tmp/bootstrap.sh"}\'\'\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/services/~w/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); RESP=$(curl -s -X POST https://api.unsandbox.com/services/~w/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); STDOUT=$(echo "$RESP" | jq -r ".stdout // empty"); if [ -n "$STDOUT" ]; then echo "$STDOUT" > "~w" && chmod 755 "~w" && echo "Bootstrap saved to ~w"; else echo -e "\\x1b[31mError: Failed to fetch bootstrap\\x1b[0m" >&2; exit 1; fi',
|
||||
[ServiceId, ServiceId, SecretKey, ServiceId, PublicKey, DumpFile, DumpFile, DumpFile])
|
||||
),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Service create
|
||||
service_create(Name, Ports, Bootstrap, ServiceType) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
% Build JSON payload
|
||||
( Ports \= ''
|
||||
-> format(atom(PortsJson), ',"ports":[~w]', [Ports])
|
||||
|
|
@ -210,25 +238,26 @@ service_create(Name, Ports, Bootstrap, ServiceType) :-
|
|||
; ServiceTypeJson = ''
|
||||
),
|
||||
format(atom(Json), '{"name":"~w"~w~w~w}', [Name, PortsJson, BootstrapJson, ServiceTypeJson]),
|
||||
% Execute curl command
|
||||
% Execute curl command with HMAC
|
||||
format(atom(Cmd),
|
||||
'curl -s -X POST https://api.unsandbox.com/services -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -d \'~w\' && echo -e "\\x1b[32mService created\\x1b[0m"',
|
||||
[ApiKey, Json]),
|
||||
'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]),
|
||||
shell(Cmd, 0).
|
||||
|
||||
% Key validate
|
||||
validate_key(Extend) :-
|
||||
get_api_key(ApiKey),
|
||||
get_public_key(PublicKey),
|
||||
get_secret_key(SecretKey),
|
||||
portal_base(PortalBase),
|
||||
( Extend = true
|
||||
-> % Build command for --extend mode
|
||||
format(atom(Cmd),
|
||||
'RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null',
|
||||
[PortalBase, ApiKey, PortalBase])
|
||||
'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null',
|
||||
[SecretKey, PortalBase, PublicKey, PortalBase])
|
||||
; % Build command for normal validation
|
||||
format(atom(Cmd),
|
||||
'curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json',
|
||||
[PortalBase, ApiKey])
|
||||
'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json',
|
||||
[SecretKey, PortalBase, PublicKey])
|
||||
),
|
||||
shell(Cmd, 0).
|
||||
|
||||
|
|
|
|||
37
un.ps1
37
un.ps1
|
|
@ -59,24 +59,47 @@ $EXT_MAP = @{
|
|||
".raku" = "raku"; ".m" = "objc"; ".awk" = "awk"
|
||||
}
|
||||
|
||||
function Get-ApiKey {
|
||||
$key = $env:UNSANDBOX_API_KEY
|
||||
if (-not $key) {
|
||||
Write-Error "Error: UNSANDBOX_API_KEY not set"
|
||||
function Get-ApiKeys {
|
||||
$publicKey = $env:UNSANDBOX_PUBLIC_KEY
|
||||
$secretKey = $env:UNSANDBOX_SECRET_KEY
|
||||
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if (-not $publicKey -and $env:UNSANDBOX_API_KEY) {
|
||||
$publicKey = $env:UNSANDBOX_API_KEY
|
||||
$secretKey = ""
|
||||
}
|
||||
|
||||
if (-not $publicKey) {
|
||||
Write-Error "Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set"
|
||||
exit 1
|
||||
}
|
||||
return $key
|
||||
return @($publicKey, $secretKey)
|
||||
}
|
||||
|
||||
function Invoke-Api {
|
||||
param($Endpoint, $Method = "GET", $Body = $null, $BaseUrl = $null)
|
||||
|
||||
$apiKey = Get-ApiKey
|
||||
$publicKey, $secretKey = Get-ApiKeys
|
||||
$headers = @{
|
||||
"Authorization" = "Bearer $apiKey"
|
||||
"Authorization" = "Bearer $publicKey"
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
|
||||
# Add HMAC signature if secret key exists
|
||||
if ($secretKey) {
|
||||
$timestamp = [int][double]::Parse((Get-Date -UFormat %s))
|
||||
$bodyContent = if ($Body) { $Body } else { "" }
|
||||
$sigInput = "${timestamp}:${Method}:${Endpoint}:${bodyContent}"
|
||||
|
||||
$hmac = New-Object System.Security.Cryptography.HMACSHA256
|
||||
$hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($secretKey)
|
||||
$hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($sigInput))
|
||||
$signature = [System.BitConverter]::ToString($hash).Replace("-", "").ToLower()
|
||||
|
||||
$headers["X-Timestamp"] = $timestamp.ToString()
|
||||
$headers["X-Signature"] = $signature
|
||||
}
|
||||
|
||||
$base = if ($BaseUrl) { $BaseUrl } else { $API_BASE }
|
||||
$uri = "$base$Endpoint"
|
||||
|
||||
|
|
|
|||
129
un.py
129
un.py
|
|
@ -59,6 +59,9 @@ import argparse
|
|||
import urllib.request
|
||||
import urllib.error
|
||||
import webbrowser
|
||||
import hmac
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
API_BASE = "https://api.unsandbox.com"
|
||||
PORTAL_BASE = "https://unsandbox.com"
|
||||
|
|
@ -86,16 +89,28 @@ EXT_MAP = {
|
|||
}
|
||||
|
||||
|
||||
def get_api_key(args_key=None):
|
||||
"""Get API key from args or environment"""
|
||||
key = args_key or os.environ.get("UNSANDBOX_API_KEY")
|
||||
if not key:
|
||||
print(f"{RED}Error: UNSANDBOX_API_KEY not set{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return key
|
||||
def get_api_keys(args_key=None):
|
||||
"""Get API keys from args or environment. Returns (public_key, secret_key)."""
|
||||
# Try new split key format first
|
||||
public_key = os.environ.get("UNSANDBOX_PUBLIC_KEY")
|
||||
secret_key = os.environ.get("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
# Fall back to old single key format for backwards compatibility
|
||||
if not public_key or not secret_key:
|
||||
old_key = args_key or os.environ.get("UNSANDBOX_API_KEY")
|
||||
if old_key:
|
||||
# Old format: use the key as secret, derive public from it or use as-is
|
||||
public_key = old_key
|
||||
secret_key = old_key
|
||||
else:
|
||||
print(f"{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set{RESET}", file=sys.stderr)
|
||||
print(f"{RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility){RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return public_key, secret_key
|
||||
|
||||
|
||||
def detect_language(filename):
|
||||
def detect_language(filename, exit_on_error=True):
|
||||
"""Detect language from file extension"""
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
lang = EXT_MAP.get(ext)
|
||||
|
|
@ -114,22 +129,52 @@ def detect_language(filename):
|
|||
if 'php' in first_line: return 'php'
|
||||
except:
|
||||
pass
|
||||
print(f"{RED}Error: Cannot detect language for {filename}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if exit_on_error:
|
||||
print(f"{RED}Error: Cannot detect language for {filename}{RESET}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return None
|
||||
return lang
|
||||
|
||||
|
||||
def api_request(endpoint, method="GET", data=None, api_key=None):
|
||||
"""Make API request and return response"""
|
||||
def read_file(filepath):
|
||||
"""Read file contents - helper for tests"""
|
||||
with open(filepath, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def execute_code(language, code, public_key=None, secret_key=None):
|
||||
"""Execute code and return result - helper for tests"""
|
||||
if not public_key:
|
||||
public_key, secret_key = get_api_keys()
|
||||
return api_request("/execute", method="POST", data={"language": language, "code": code}, public_key=public_key, secret_key=secret_key)
|
||||
|
||||
|
||||
def api_request(endpoint, method="GET", data=None, public_key=None, secret_key=None):
|
||||
"""Make API request with HMAC authentication"""
|
||||
url = f"{API_BASE}{endpoint}"
|
||||
|
||||
# Prepare body
|
||||
body = json.dumps(data) if data else ""
|
||||
|
||||
# Generate HMAC signature
|
||||
timestamp = str(int(time.time()))
|
||||
signature_input = f"{timestamp}:{method}:{endpoint}:{body}"
|
||||
signature = hmac.new(
|
||||
secret_key.encode('utf-8'),
|
||||
signature_input.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Authorization": f"Bearer {public_key}",
|
||||
"X-Timestamp": timestamp,
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
req = urllib.request.Request(url, method=method, headers=headers)
|
||||
if data:
|
||||
req.data = json.dumps(data).encode('utf-8')
|
||||
req.data = body.encode('utf-8')
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=300) as resp:
|
||||
|
|
@ -145,7 +190,7 @@ def api_request(endpoint, method="GET", data=None, api_key=None):
|
|||
|
||||
def cmd_execute(args):
|
||||
"""Execute source code"""
|
||||
api_key = get_api_key(args.api_key)
|
||||
public_key, secret_key = get_api_keys(args.api_key)
|
||||
|
||||
# Read source file
|
||||
try:
|
||||
|
|
@ -199,7 +244,7 @@ def cmd_execute(args):
|
|||
payload["vcpu"] = args.vcpu
|
||||
|
||||
# Execute
|
||||
result = api_request("/execute", method="POST", data=payload, api_key=api_key)
|
||||
result = api_request("/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||
|
||||
# Print output
|
||||
if result.get("stdout"):
|
||||
|
|
@ -225,10 +270,10 @@ def cmd_execute(args):
|
|||
|
||||
def cmd_session(args):
|
||||
"""Manage interactive sessions"""
|
||||
api_key = get_api_key(args.api_key)
|
||||
public_key, secret_key = get_api_keys(args.api_key)
|
||||
|
||||
if args.list:
|
||||
result = api_request("/sessions", api_key=api_key)
|
||||
result = api_request("/sessions", public_key=public_key, secret_key=secret_key)
|
||||
sessions = result.get("sessions", [])
|
||||
if not sessions:
|
||||
print("No active sessions")
|
||||
|
|
@ -239,7 +284,7 @@ def cmd_session(args):
|
|||
return
|
||||
|
||||
if args.kill:
|
||||
result = api_request(f"/sessions/{args.kill}", method="DELETE", api_key=api_key)
|
||||
result = api_request(f"/sessions/{args.kill}", method="DELETE", public_key=public_key, secret_key=secret_key)
|
||||
print(f"{GREEN}Session terminated: {args.kill}{RESET}")
|
||||
return
|
||||
|
||||
|
|
@ -264,16 +309,30 @@ def cmd_session(args):
|
|||
payload["audit"] = True
|
||||
|
||||
print(f"{YELLOW}Creating session...{RESET}")
|
||||
result = api_request("/sessions", method="POST", data=payload, api_key=api_key)
|
||||
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}")
|
||||
print(f"{YELLOW}(Interactive sessions require WebSocket - use un2 for full support){RESET}")
|
||||
|
||||
|
||||
def validate_key(api_key, extend=False):
|
||||
def validate_key(public_key, secret_key, extend=False):
|
||||
"""Validate API key and display information"""
|
||||
url = f"{PORTAL_BASE}/keys/validate"
|
||||
|
||||
# Generate HMAC signature for portal request
|
||||
timestamp = str(int(time.time()))
|
||||
endpoint = "/keys/validate"
|
||||
body = ""
|
||||
signature_input = f"{timestamp}:POST:{endpoint}:{body}"
|
||||
signature = hmac.new(
|
||||
secret_key.encode('utf-8'),
|
||||
signature_input.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Authorization": f"Bearer {public_key}",
|
||||
"X-Timestamp": timestamp,
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
|
|
@ -332,16 +391,16 @@ def validate_key(api_key, extend=False):
|
|||
|
||||
def cmd_key(args):
|
||||
"""Validate API key"""
|
||||
api_key = get_api_key(args.key)
|
||||
validate_key(api_key, extend=args.extend)
|
||||
public_key, secret_key = get_api_keys(args.key)
|
||||
validate_key(public_key, secret_key, extend=args.extend)
|
||||
|
||||
|
||||
def cmd_service(args):
|
||||
"""Manage persistent services"""
|
||||
api_key = get_api_key(args.api_key)
|
||||
public_key, secret_key = get_api_keys(args.api_key)
|
||||
|
||||
if args.list:
|
||||
result = api_request("/services", api_key=api_key)
|
||||
result = api_request("/services", public_key=public_key, secret_key=secret_key)
|
||||
services = result.get("services", [])
|
||||
if not services:
|
||||
print("No services")
|
||||
|
|
@ -354,38 +413,38 @@ def cmd_service(args):
|
|||
return
|
||||
|
||||
if args.info:
|
||||
result = api_request(f"/services/{args.info}", api_key=api_key)
|
||||
result = api_request(f"/services/{args.info}", public_key=public_key, secret_key=secret_key)
|
||||
print(json.dumps(result, indent=2))
|
||||
return
|
||||
|
||||
if args.logs:
|
||||
result = api_request(f"/services/{args.logs}/logs", api_key=api_key)
|
||||
result = api_request(f"/services/{args.logs}/logs", public_key=public_key, secret_key=secret_key)
|
||||
print(result.get("logs", ""))
|
||||
return
|
||||
|
||||
if args.tail:
|
||||
result = api_request(f"/services/{args.tail}/logs?lines=9000", api_key=api_key)
|
||||
result = api_request(f"/services/{args.tail}/logs?lines=9000", public_key=public_key, secret_key=secret_key)
|
||||
print(result.get("logs", ""))
|
||||
return
|
||||
|
||||
if args.sleep:
|
||||
result = api_request(f"/services/{args.sleep}/sleep", method="POST", api_key=api_key)
|
||||
result = api_request(f"/services/{args.sleep}/sleep", method="POST", public_key=public_key, secret_key=secret_key)
|
||||
print(f"{GREEN}Service sleeping: {args.sleep}{RESET}")
|
||||
return
|
||||
|
||||
if args.wake:
|
||||
result = api_request(f"/services/{args.wake}/wake", method="POST", api_key=api_key)
|
||||
result = api_request(f"/services/{args.wake}/wake", method="POST", public_key=public_key, secret_key=secret_key)
|
||||
print(f"{GREEN}Service waking: {args.wake}{RESET}")
|
||||
return
|
||||
|
||||
if args.destroy:
|
||||
result = api_request(f"/services/{args.destroy}", method="DELETE", api_key=api_key)
|
||||
result = api_request(f"/services/{args.destroy}", method="DELETE", public_key=public_key, secret_key=secret_key)
|
||||
print(f"{GREEN}Service destroyed: {args.destroy}{RESET}")
|
||||
return
|
||||
|
||||
if args.execute:
|
||||
payload = {"command": args.command}
|
||||
result = api_request(f"/services/{args.execute}/execute", method="POST", data=payload, api_key=api_key)
|
||||
result = api_request(f"/services/{args.execute}/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||
if result.get("stdout"):
|
||||
print(f"{BLUE}{result['stdout']}{RESET}", end='')
|
||||
if result.get("stderr"):
|
||||
|
|
@ -395,7 +454,7 @@ def cmd_service(args):
|
|||
if args.dump_bootstrap:
|
||||
print(f"Fetching bootstrap script from {args.dump_bootstrap}...", file=sys.stderr)
|
||||
payload = {"command": "cat /tmp/bootstrap.sh"}
|
||||
result = api_request(f"/services/{args.dump_bootstrap}/execute", method="POST", data=payload, api_key=api_key)
|
||||
result = api_request(f"/services/{args.dump_bootstrap}/execute", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||
|
||||
if result.get("stdout"):
|
||||
bootstrap = result["stdout"]
|
||||
|
|
@ -438,7 +497,7 @@ def cmd_service(args):
|
|||
if args.vcpu:
|
||||
payload["vcpu"] = args.vcpu
|
||||
|
||||
result = api_request("/services", method="POST", data=payload, api_key=api_key)
|
||||
result = api_request("/services", method="POST", data=payload, public_key=public_key, secret_key=secret_key)
|
||||
print(f"{GREEN}Service created: {result.get('id', 'N/A')}{RESET}")
|
||||
print(f"Name: {result.get('name', 'N/A')}")
|
||||
if result.get('url'):
|
||||
|
|
|
|||
112
un.r
112
un.r
|
|
@ -38,6 +38,7 @@
|
|||
|
||||
library(httr)
|
||||
library(jsonlite)
|
||||
library(digest)
|
||||
|
||||
# Extension to language mapping
|
||||
ext_map <- list(
|
||||
|
|
@ -75,28 +76,53 @@ detect_language <- function(filename) {
|
|||
return(lang)
|
||||
}
|
||||
|
||||
get_api_key <- function(args_key = NULL) {
|
||||
key <- if (!is.null(args_key)) args_key else Sys.getenv("UNSANDBOX_API_KEY")
|
||||
if (key == "") {
|
||||
cat(sprintf("%sError: UNSANDBOX_API_KEY not set%s\n", RED, RESET), file = stderr())
|
||||
get_api_keys <- function(args_key = NULL) {
|
||||
public_key <- Sys.getenv("UNSANDBOX_PUBLIC_KEY")
|
||||
secret_key <- Sys.getenv("UNSANDBOX_SECRET_KEY")
|
||||
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if (public_key == "" && Sys.getenv("UNSANDBOX_API_KEY") != "") {
|
||||
public_key <- Sys.getenv("UNSANDBOX_API_KEY")
|
||||
secret_key <- ""
|
||||
}
|
||||
|
||||
if (public_key == "") {
|
||||
cat(sprintf("%sError: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set%s\n", RED, RESET), file = stderr())
|
||||
quit(status = 1)
|
||||
}
|
||||
return(key)
|
||||
return(list(public_key = public_key, secret_key = secret_key))
|
||||
}
|
||||
|
||||
api_request <- function(endpoint, api_key, method = "GET", data = NULL) {
|
||||
api_request <- function(endpoint, public_key, secret_key, method = "GET", data = NULL) {
|
||||
url <- paste0(API_BASE, endpoint)
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", api_key)
|
||||
`Authorization` = paste("Bearer", public_key)
|
||||
)
|
||||
|
||||
body_content <- ""
|
||||
if (!is.null(data)) {
|
||||
body_content <- toJSON(data, auto_unbox = TRUE)
|
||||
}
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if (secret_key != "") {
|
||||
timestamp <- as.integer(Sys.time())
|
||||
sig_input <- paste0(timestamp, ":", method, ":", endpoint, ":", body_content)
|
||||
signature <- hmac(sig_input, secret_key, algo = "sha256")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", public_key),
|
||||
`X-Timestamp` = as.character(timestamp),
|
||||
`X-Signature` = signature
|
||||
)
|
||||
}
|
||||
|
||||
tryCatch({
|
||||
if (method == "GET") {
|
||||
response <- GET(url, headers, timeout(300))
|
||||
} else if (method == "POST") {
|
||||
body <- if (!is.null(data)) toJSON(data, auto_unbox = TRUE) else ""
|
||||
response <- POST(url, headers, body = body, encode = "raw", timeout(300))
|
||||
response <- POST(url, headers, body = body_content, encode = "raw", timeout(300))
|
||||
} else if (method == "DELETE") {
|
||||
response <- DELETE(url, headers, timeout(300))
|
||||
} else {
|
||||
|
|
@ -112,7 +138,9 @@ api_request <- function(endpoint, api_key, method = "GET", data = NULL) {
|
|||
}
|
||||
|
||||
cmd_execute <- function(args) {
|
||||
api_key <- get_api_key(args$api_key)
|
||||
keys <- get_api_keys(args$api_key)
|
||||
public_key <- keys$public_key
|
||||
secret_key <- keys$secret_key
|
||||
|
||||
filename <- args$source_file
|
||||
if (!file.exists(filename)) {
|
||||
|
|
@ -175,7 +203,7 @@ cmd_execute <- function(args) {
|
|||
}
|
||||
|
||||
# Execute
|
||||
result <- api_request("/execute", api_key, method = "POST", data = payload)
|
||||
result <- api_request("/execute", public_key, secret_key, method = "POST", data = payload)
|
||||
|
||||
# Print output
|
||||
if (!is.null(result$stdout) && result$stdout != "") {
|
||||
|
|
@ -204,10 +232,12 @@ cmd_execute <- function(args) {
|
|||
}
|
||||
|
||||
cmd_session <- function(args) {
|
||||
api_key <- get_api_key(args$api_key)
|
||||
keys <- get_api_keys(args$api_key)
|
||||
public_key <- keys$public_key
|
||||
secret_key <- keys$secret_key
|
||||
|
||||
if (!is.null(args$list) && args$list) {
|
||||
result <- api_request("/sessions", api_key)
|
||||
result <- api_request("/sessions", public_key, secret_key)
|
||||
sessions <- if (!is.null(result$sessions)) result$sessions else list()
|
||||
if (length(sessions) == 0) {
|
||||
cat("No active sessions\n")
|
||||
|
|
@ -225,7 +255,7 @@ cmd_session <- function(args) {
|
|||
}
|
||||
|
||||
if (!is.null(args$kill)) {
|
||||
result <- api_request(paste0("/sessions/", args$kill), api_key, method = "DELETE")
|
||||
result <- api_request(paste0("/sessions/", args$kill), public_key, secret_key, method = "DELETE")
|
||||
cat(sprintf("%sSession terminated: %s%s\n", GREEN, args$kill, RESET))
|
||||
return()
|
||||
}
|
||||
|
|
@ -235,16 +265,31 @@ cmd_session <- function(args) {
|
|||
}
|
||||
|
||||
cmd_key <- function(args) {
|
||||
api_key <- get_api_key(args$api_key)
|
||||
keys <- get_api_keys(args$api_key)
|
||||
public_key <- keys$public_key
|
||||
secret_key <- keys$secret_key
|
||||
|
||||
if (!is.null(args$extend) && args$extend) {
|
||||
# First validate to get public_key
|
||||
url <- paste0(PORTAL_BASE, "/keys/validate")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", api_key)
|
||||
`Authorization` = paste("Bearer", public_key)
|
||||
)
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if (secret_key != "") {
|
||||
timestamp <- as.integer(Sys.time())
|
||||
sig_input <- paste0(timestamp, ":POST:/keys/validate:")
|
||||
signature <- hmac(sig_input, secret_key, algo = "sha256")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", public_key),
|
||||
`X-Timestamp` = as.character(timestamp),
|
||||
`X-Signature` = signature
|
||||
)
|
||||
}
|
||||
|
||||
tryCatch({
|
||||
response <- POST(url, headers, encode = "json", timeout(10))
|
||||
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
|
||||
|
|
@ -268,9 +313,22 @@ cmd_key <- function(args) {
|
|||
url <- paste0(PORTAL_BASE, "/keys/validate")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", api_key)
|
||||
`Authorization` = paste("Bearer", public_key)
|
||||
)
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if (secret_key != "") {
|
||||
timestamp <- as.integer(Sys.time())
|
||||
sig_input <- paste0(timestamp, ":POST:/keys/validate:")
|
||||
signature <- hmac(sig_input, secret_key, algo = "sha256")
|
||||
headers <- add_headers(
|
||||
`Content-Type` = "application/json",
|
||||
`Authorization` = paste("Bearer", public_key),
|
||||
`X-Timestamp` = as.character(timestamp),
|
||||
`X-Signature` = signature
|
||||
)
|
||||
}
|
||||
|
||||
tryCatch({
|
||||
response <- POST(url, headers, encode = "json", timeout(10))
|
||||
result <- fromJSON(content(response, "text", encoding = "UTF-8"))
|
||||
|
|
@ -302,10 +360,12 @@ cmd_key <- function(args) {
|
|||
}
|
||||
|
||||
cmd_service <- function(args) {
|
||||
api_key <- get_api_key(args$api_key)
|
||||
keys <- get_api_keys(args$api_key)
|
||||
public_key <- keys$public_key
|
||||
secret_key <- keys$secret_key
|
||||
|
||||
if (!is.null(args$list) && args$list) {
|
||||
result <- api_request("/services", api_key)
|
||||
result <- api_request("/services", public_key, secret_key)
|
||||
services <- if (!is.null(result$services)) result$services else list()
|
||||
if (length(services) == 0) {
|
||||
cat("No services\n")
|
||||
|
|
@ -325,31 +385,31 @@ cmd_service <- function(args) {
|
|||
}
|
||||
|
||||
if (!is.null(args$info)) {
|
||||
result <- api_request(paste0("/services/", args$info), api_key)
|
||||
result <- api_request(paste0("/services/", args$info), public_key, secret_key)
|
||||
cat(toJSON(result, pretty = TRUE, auto_unbox = TRUE), "\n")
|
||||
return()
|
||||
}
|
||||
|
||||
if (!is.null(args$logs)) {
|
||||
result <- api_request(paste0("/services/", args$logs, "/logs"), api_key)
|
||||
result <- api_request(paste0("/services/", args$logs, "/logs"), public_key, secret_key)
|
||||
cat(if (!is.null(result$logs)) result$logs else "", "\n")
|
||||
return()
|
||||
}
|
||||
|
||||
if (!is.null(args$sleep)) {
|
||||
result <- api_request(paste0("/services/", args$sleep, "/sleep"), api_key, method = "POST")
|
||||
result <- api_request(paste0("/services/", args$sleep, "/sleep"), public_key, secret_key, method = "POST")
|
||||
cat(sprintf("%sService sleeping: %s%s\n", GREEN, args$sleep, RESET))
|
||||
return()
|
||||
}
|
||||
|
||||
if (!is.null(args$wake)) {
|
||||
result <- api_request(paste0("/services/", args$wake, "/wake"), api_key, method = "POST")
|
||||
result <- api_request(paste0("/services/", args$wake, "/wake"), public_key, secret_key, method = "POST")
|
||||
cat(sprintf("%sService waking: %s%s\n", GREEN, args$wake, RESET))
|
||||
return()
|
||||
}
|
||||
|
||||
if (!is.null(args$destroy)) {
|
||||
result <- api_request(paste0("/services/", args$destroy), api_key, method = "DELETE")
|
||||
result <- api_request(paste0("/services/", args$destroy), public_key, secret_key, method = "DELETE")
|
||||
cat(sprintf("%sService destroyed: %s%s\n", GREEN, args$destroy, RESET))
|
||||
return()
|
||||
}
|
||||
|
|
@ -357,7 +417,7 @@ cmd_service <- function(args) {
|
|||
if (!is.null(args$dump_bootstrap)) {
|
||||
cat(sprintf("Fetching bootstrap script from %s...\n", args$dump_bootstrap), file = stderr())
|
||||
payload <- list(command = "cat /tmp/bootstrap.sh")
|
||||
result <- api_request(paste0("/services/", args$dump_bootstrap, "/execute"), api_key, method = "POST", data = payload)
|
||||
result <- api_request(paste0("/services/", args$dump_bootstrap, "/execute"), public_key, secret_key, method = "POST", data = payload)
|
||||
|
||||
if (!is.null(result$stdout) && result$stdout != "") {
|
||||
bootstrap <- result$stdout
|
||||
|
|
@ -415,7 +475,7 @@ cmd_service <- function(args) {
|
|||
payload$vcpu <- args$vcpu
|
||||
}
|
||||
|
||||
result <- api_request("/services", api_key, method = "POST", data = payload)
|
||||
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"))
|
||||
if (!is.null(result$url)) {
|
||||
|
|
|
|||
80
un.raku
80
un.raku
|
|
@ -40,6 +40,7 @@
|
|||
# Full-featured CLI matching un.c/un.py capabilities
|
||||
|
||||
use JSON::Fast;
|
||||
use Digest::SHA;
|
||||
|
||||
constant $API_BASE = "https://api.unsandbox.com";
|
||||
constant $PORTAL_BASE = "https://unsandbox.com";
|
||||
|
|
@ -66,13 +67,21 @@ my %EXT_MAP = (
|
|||
m => 'objc'
|
||||
);
|
||||
|
||||
sub get-api-key() {
|
||||
my $key = %*ENV<UNSANDBOX_API_KEY>;
|
||||
unless $key {
|
||||
note "{$RED}Error: UNSANDBOX_API_KEY not set{$RESET}";
|
||||
sub get-api-keys() {
|
||||
my $public-key = %*ENV<UNSANDBOX_PUBLIC_KEY> // '';
|
||||
my $secret-key = %*ENV<UNSANDBOX_SECRET_KEY> // '';
|
||||
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if !$public-key && %*ENV<UNSANDBOX_API_KEY> {
|
||||
$public-key = %*ENV<UNSANDBOX_API_KEY>;
|
||||
$secret-key = '';
|
||||
}
|
||||
|
||||
unless $public-key {
|
||||
note "{$RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set{$RESET}";
|
||||
exit 1;
|
||||
}
|
||||
return $key;
|
||||
return ($public-key, $secret-key);
|
||||
}
|
||||
|
||||
sub detect-language(Str $filename --> Str) {
|
||||
|
|
@ -98,9 +107,10 @@ sub detect-language(Str $filename --> Str) {
|
|||
exit 1;
|
||||
}
|
||||
|
||||
sub api-request(Str $endpoint, Str $method, %data?, Str :$api-key!) {
|
||||
sub api-request(Str $endpoint, Str $method, %data?, Str :$public-key!, Str :$secret-key!) {
|
||||
my $url = $API_BASE ~ $endpoint;
|
||||
my @args = 'curl', '-s';
|
||||
my $body = '';
|
||||
|
||||
if $method eq 'GET' {
|
||||
@args.append: '-X', 'GET';
|
||||
|
|
@ -110,11 +120,22 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$api-key!) {
|
|||
@args.append: '-X', 'POST';
|
||||
@args.append: '-H', 'Content-Type: application/json';
|
||||
if %data {
|
||||
@args.append: '-d', to-json(%data);
|
||||
$body = to-json(%data);
|
||||
@args.append: '-d', $body;
|
||||
}
|
||||
}
|
||||
|
||||
@args.append: '-H', "Authorization: Bearer $api-key";
|
||||
@args.append: '-H', "Authorization: Bearer $public-key";
|
||||
|
||||
# Add HMAC signature if secret-key is present
|
||||
if $secret-key {
|
||||
my $timestamp = now.Int;
|
||||
my $sig-input = "{$timestamp}:{$method}:{$endpoint}:{$body}";
|
||||
my $signature = hmac-hex($sig-input, $secret-key, &sha256);
|
||||
@args.append: '-H', "X-Timestamp: $timestamp";
|
||||
@args.append: '-H', "X-Signature: $signature";
|
||||
}
|
||||
|
||||
@args.append: $url;
|
||||
|
||||
my $proc = run |@args, :out, :err;
|
||||
|
|
@ -131,7 +152,7 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$api-key!) {
|
|||
}
|
||||
|
||||
sub cmd-execute(@args) {
|
||||
my $api-key = get-api-key();
|
||||
my ($public-key, $secret-key) = get-api-keys();
|
||||
my $source-file = '';
|
||||
my %env-vars;
|
||||
my @input-files;
|
||||
|
|
@ -218,7 +239,7 @@ sub cmd-execute(@args) {
|
|||
%payload<vcpu> = $vcpu if $vcpu > 0;
|
||||
|
||||
# Execute
|
||||
my %result = api-request('/execute', 'POST', %payload, :$api-key);
|
||||
my %result = api-request('/execute', 'POST', %payload, :$public-key, :$secret-key);
|
||||
|
||||
# Print output
|
||||
if %result<stdout> {
|
||||
|
|
@ -245,7 +266,7 @@ sub cmd-execute(@args) {
|
|||
}
|
||||
|
||||
sub cmd-session(@args) {
|
||||
my $api-key = get-api-key();
|
||||
my ($public-key, $secret-key) = get-api-keys();
|
||||
my $list-mode = False;
|
||||
my $kill-id = '';
|
||||
my $shell = '';
|
||||
|
|
@ -280,7 +301,7 @@ sub cmd-session(@args) {
|
|||
}
|
||||
|
||||
if $list-mode {
|
||||
my %result = api-request('/sessions', 'GET', :$api-key);
|
||||
my %result = api-request('/sessions', 'GET', :$public-key, :$secret-key);
|
||||
my @sessions = %result<sessions>.list;
|
||||
unless @sessions {
|
||||
say "No active sessions";
|
||||
|
|
@ -295,7 +316,7 @@ sub cmd-session(@args) {
|
|||
}
|
||||
|
||||
if $kill-id {
|
||||
api-request("/sessions/$kill-id", 'DELETE', :$api-key);
|
||||
api-request("/sessions/$kill-id", 'DELETE', :$public-key, :$secret-key);
|
||||
say "{$GREEN}Session terminated: $kill-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
|
@ -306,13 +327,13 @@ sub cmd-session(@args) {
|
|||
%payload<vcpu> = $vcpu if $vcpu > 0;
|
||||
|
||||
say "{$YELLOW}Creating session...{$RESET}";
|
||||
my %result = api-request('/sessions', 'POST', %payload, :$api-key);
|
||||
my %result = api-request('/sessions', 'POST', %payload, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Session created: {%result<id>}{$RESET}";
|
||||
say "{$YELLOW}(Interactive sessions require WebSocket - use un2 for full support){$RESET}";
|
||||
}
|
||||
|
||||
sub cmd-service(@args) {
|
||||
my $api-key = get-api-key();
|
||||
my ($public-key, $secret-key) = get-api-keys();
|
||||
my $list-mode = False;
|
||||
my $info-id = '';
|
||||
my $logs-id = '';
|
||||
|
|
@ -392,7 +413,7 @@ sub cmd-service(@args) {
|
|||
}
|
||||
|
||||
if $list-mode {
|
||||
my %result = api-request('/services', 'GET', :$api-key);
|
||||
my %result = api-request('/services', 'GET', :$public-key, :$secret-key);
|
||||
my @services = %result<services>.list;
|
||||
unless @services {
|
||||
say "No services";
|
||||
|
|
@ -409,31 +430,31 @@ sub cmd-service(@args) {
|
|||
}
|
||||
|
||||
if $info-id {
|
||||
my %result = api-request("/services/$info-id", 'GET', :$api-key);
|
||||
my %result = api-request("/services/$info-id", 'GET', :$public-key, :$secret-key);
|
||||
say to-json(%result, :pretty);
|
||||
return;
|
||||
}
|
||||
|
||||
if $logs-id {
|
||||
my %result = api-request("/services/$logs-id/logs", 'GET', :$api-key);
|
||||
my %result = api-request("/services/$logs-id/logs", 'GET', :$public-key, :$secret-key);
|
||||
say %result<logs>;
|
||||
return;
|
||||
}
|
||||
|
||||
if $sleep-id {
|
||||
api-request("/services/$sleep-id/sleep", 'POST', :$api-key);
|
||||
api-request("/services/$sleep-id/sleep", 'POST', :$public-key, :$secret-key);
|
||||
say "{$GREEN}Service sleeping: $sleep-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
if $wake-id {
|
||||
api-request("/services/$wake-id/wake", 'POST', :$api-key);
|
||||
api-request("/services/$wake-id/wake", 'POST', :$public-key, :$secret-key);
|
||||
say "{$GREEN}Service waking: $wake-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
||||
if $destroy-id {
|
||||
api-request("/services/$destroy-id", 'DELETE', :$api-key);
|
||||
api-request("/services/$destroy-id", 'DELETE', :$public-key, :$secret-key);
|
||||
say "{$GREEN}Service destroyed: $destroy-id{$RESET}";
|
||||
return;
|
||||
}
|
||||
|
|
@ -441,7 +462,7 @@ sub cmd-service(@args) {
|
|||
if $dump-bootstrap-id {
|
||||
note "Fetching bootstrap script from $dump-bootstrap-id...";
|
||||
my %payload = command => "cat /tmp/bootstrap.sh";
|
||||
my %result = api-request("/services/$dump-bootstrap-id/execute", 'POST', %payload, :$api-key);
|
||||
my %result = api-request("/services/$dump-bootstrap-id/execute", 'POST', %payload, :$public-key, :$secret-key);
|
||||
|
||||
if %result<stdout> && %result<stdout> ne '' {
|
||||
my $bootstrap = %result<stdout>;
|
||||
|
|
@ -485,7 +506,7 @@ sub cmd-service(@args) {
|
|||
%payload<network> = $network if $network;
|
||||
%payload<vcpu> = $vcpu if $vcpu > 0;
|
||||
|
||||
my %result = api-request('/services', 'POST', %payload, :$api-key);
|
||||
my %result = api-request('/services', 'POST', %payload, :$public-key, :$secret-key);
|
||||
say "{$GREEN}Service created: {%result<id>}{$RESET}";
|
||||
say "Name: {%result<name>}";
|
||||
say "URL: {%result<url>}" if %result<url>;
|
||||
|
|
@ -497,13 +518,22 @@ sub cmd-service(@args) {
|
|||
}
|
||||
|
||||
sub validate-key(Bool $extend) {
|
||||
my $api-key = get-api-key();
|
||||
my ($public-key, $secret-key) = get-api-keys();
|
||||
|
||||
# Build curl command
|
||||
my @args = 'curl', '-s', '-X', 'POST';
|
||||
@args.append: "$PORTAL_BASE/keys/validate";
|
||||
@args.append: '-H', 'Content-Type: application/json';
|
||||
@args.append: '-H', "Authorization: Bearer $api-key";
|
||||
@args.append: '-H', "Authorization: Bearer $public-key";
|
||||
|
||||
# Add HMAC signature if secret-key is present
|
||||
if $secret-key {
|
||||
my $timestamp = now.Int;
|
||||
my $sig-input = "{$timestamp}:POST:/keys/validate:";
|
||||
my $signature = hmac-hex($sig-input, $secret-key, &sha256);
|
||||
@args.append: '-H', "X-Timestamp: $timestamp";
|
||||
@args.append: '-H', "X-Signature: $signature";
|
||||
}
|
||||
|
||||
my $proc = run |@args, :out, :err;
|
||||
my $body = $proc.out.slurp;
|
||||
|
|
|
|||
90
un.rb
90
un.rb
|
|
@ -80,13 +80,23 @@ EXT_MAP = {
|
|||
'.tcl' => 'tcl', '.raku' => 'raku', '.m' => 'objc'
|
||||
}.freeze
|
||||
|
||||
def get_api_key(args_key = nil)
|
||||
key = args_key || ENV['UNSANDBOX_API_KEY']
|
||||
unless key
|
||||
warn "#{RED}Error: UNSANDBOX_API_KEY not set#{RESET}"
|
||||
exit 1
|
||||
def get_api_keys(args_key = nil)
|
||||
public_key = ENV['UNSANDBOX_PUBLIC_KEY']
|
||||
secret_key = ENV['UNSANDBOX_SECRET_KEY']
|
||||
|
||||
unless public_key && secret_key
|
||||
old_key = args_key || ENV['UNSANDBOX_API_KEY']
|
||||
if old_key
|
||||
public_key = old_key
|
||||
secret_key = old_key
|
||||
else
|
||||
warn "#{RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set#{RESET}"
|
||||
warn "#{RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)#{RESET}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
key
|
||||
|
||||
{ public_key: public_key, secret_key: secret_key }
|
||||
end
|
||||
|
||||
def detect_language(filename)
|
||||
|
|
@ -112,12 +122,19 @@ def detect_language(filename)
|
|||
lang
|
||||
end
|
||||
|
||||
def api_request(endpoint, method: 'GET', data: nil, api_key:)
|
||||
def api_request(endpoint, method: 'GET', data: nil, keys:)
|
||||
require 'openssl'
|
||||
|
||||
uri = URI("#{API_BASE}#{endpoint}")
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
http.read_timeout = 300
|
||||
|
||||
timestamp = Time.now.to_i.to_s
|
||||
body = data ? JSON.generate(data) : ''
|
||||
message = "#{timestamp}:#{method}:#{uri.path}#{uri.query ? "?#{uri.query}" : ''}:#{body}"
|
||||
signature = OpenSSL::HMAC.hexdigest('SHA256', keys[:secret_key], message)
|
||||
|
||||
request = case method
|
||||
when 'GET' then Net::HTTP::Get.new(uri)
|
||||
when 'POST' then Net::HTTP::Post.new(uri)
|
||||
|
|
@ -125,9 +142,11 @@ def api_request(endpoint, method: 'GET', data: nil, api_key:)
|
|||
else raise "Unknown method: #{method}"
|
||||
end
|
||||
|
||||
request['Authorization'] = "Bearer #{api_key}"
|
||||
request['Authorization'] = "Bearer #{keys[:public_key]}"
|
||||
request['X-Timestamp'] = timestamp
|
||||
request['X-Signature'] = signature
|
||||
request['Content-Type'] = 'application/json'
|
||||
request.body = JSON.generate(data) if data
|
||||
request.body = body if data
|
||||
|
||||
response = http.request(request)
|
||||
unless response.is_a?(Net::HTTPSuccess)
|
||||
|
|
@ -142,7 +161,7 @@ rescue => e
|
|||
end
|
||||
|
||||
def cmd_execute(options)
|
||||
api_key = get_api_key(options[:api_key])
|
||||
keys = get_api_keys(options[:api_key])
|
||||
|
||||
unless File.exist?(options[:source_file])
|
||||
warn "#{RED}Error: File not found: #{options[:source_file]}#{RESET}"
|
||||
|
|
@ -181,7 +200,7 @@ def cmd_execute(options)
|
|||
payload[:network] = options[:network] if options[:network]
|
||||
payload[:vcpu] = options[:vcpu] if options[:vcpu]
|
||||
|
||||
result = api_request('/execute', method: 'POST', data: payload, api_key: api_key)
|
||||
result = api_request('/execute', method: 'POST', data: payload, keys: keys)
|
||||
|
||||
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
|
||||
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
|
||||
|
|
@ -203,10 +222,10 @@ def cmd_execute(options)
|
|||
end
|
||||
|
||||
def cmd_session(options)
|
||||
api_key = get_api_key(options[:api_key])
|
||||
keys = get_api_keys(options[:api_key])
|
||||
|
||||
if options[:list]
|
||||
result = api_request('/sessions', api_key: api_key)
|
||||
result = api_request('/sessions', keys: keys)
|
||||
sessions = result['sessions'] || []
|
||||
if sessions.empty?
|
||||
puts 'No active sessions'
|
||||
|
|
@ -222,7 +241,7 @@ def cmd_session(options)
|
|||
end
|
||||
|
||||
if options[:kill]
|
||||
api_request("/sessions/#{options[:kill]}", method: 'DELETE', api_key: api_key)
|
||||
api_request("/sessions/#{options[:kill]}", method: 'DELETE', keys: keys)
|
||||
puts "#{GREEN}Session terminated: #{options[:kill]}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
|
@ -241,19 +260,28 @@ def cmd_session(options)
|
|||
payload[:audit] = true if options[:audit]
|
||||
|
||||
puts "#{YELLOW}Creating session...#{RESET}"
|
||||
result = api_request('/sessions', method: 'POST', data: payload, api_key: api_key)
|
||||
result = api_request('/sessions', method: 'POST', data: payload, keys: keys)
|
||||
puts "#{GREEN}Session created: #{result['id'] || 'N/A'}#{RESET}"
|
||||
puts "#{YELLOW}(Interactive sessions require WebSocket - use un2 for full support)#{RESET}"
|
||||
end
|
||||
|
||||
def validate_key(api_key)
|
||||
def validate_key(keys)
|
||||
require 'openssl'
|
||||
|
||||
uri = URI("#{PORTAL_BASE}/keys/validate")
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
http.read_timeout = 30
|
||||
|
||||
timestamp = Time.now.to_i.to_s
|
||||
body = ''
|
||||
message = "#{timestamp}:POST:#{uri.path}:#{body}"
|
||||
signature = OpenSSL::HMAC.hexdigest('SHA256', keys[:secret_key], message)
|
||||
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request['Authorization'] = "Bearer #{api_key}"
|
||||
request['Authorization'] = "Bearer #{keys[:public_key]}"
|
||||
request['X-Timestamp'] = timestamp
|
||||
request['X-Signature'] = signature
|
||||
request['Content-Type'] = 'application/json'
|
||||
|
||||
response = http.request(request)
|
||||
|
|
@ -308,10 +336,10 @@ def open_browser(url)
|
|||
end
|
||||
|
||||
def cmd_key(options)
|
||||
api_key = get_api_key(options[:api_key])
|
||||
keys = get_api_keys(options[:api_key])
|
||||
|
||||
if options[:extend]
|
||||
result = validate_key(api_key)
|
||||
result = validate_key(keys)
|
||||
public_key = result['public_key']
|
||||
if public_key
|
||||
url = "#{PORTAL_BASE}/keys/extend?pk=#{public_key}"
|
||||
|
|
@ -322,15 +350,15 @@ def cmd_key(options)
|
|||
exit 1
|
||||
end
|
||||
else
|
||||
validate_key(api_key)
|
||||
validate_key(keys)
|
||||
end
|
||||
end
|
||||
|
||||
def cmd_service(options)
|
||||
api_key = get_api_key(options[:api_key])
|
||||
keys = get_api_keys(options[:api_key])
|
||||
|
||||
if options[:list]
|
||||
result = api_request('/services', api_key: api_key)
|
||||
result = api_request('/services', keys: keys)
|
||||
services = result['services'] || []
|
||||
if services.empty?
|
||||
puts 'No services'
|
||||
|
|
@ -348,44 +376,44 @@ def cmd_service(options)
|
|||
end
|
||||
|
||||
if options[:info]
|
||||
result = api_request("/services/#{options[:info]}", api_key: api_key)
|
||||
result = api_request("/services/#{options[:info]}", keys: keys)
|
||||
puts JSON.pretty_generate(result)
|
||||
return
|
||||
end
|
||||
|
||||
if options[:logs]
|
||||
result = api_request("/services/#{options[:logs]}/logs", api_key: api_key)
|
||||
result = api_request("/services/#{options[:logs]}/logs", keys: keys)
|
||||
puts result['logs'] || ''
|
||||
return
|
||||
end
|
||||
|
||||
if options[:tail]
|
||||
result = api_request("/services/#{options[:tail]}/logs?lines=9000", api_key: api_key)
|
||||
result = api_request("/services/#{options[:tail]}/logs?lines=9000", keys: keys)
|
||||
puts result['logs'] || ''
|
||||
return
|
||||
end
|
||||
|
||||
if options[:sleep]
|
||||
api_request("/services/#{options[:sleep]}/sleep", method: 'POST', api_key: api_key)
|
||||
api_request("/services/#{options[:sleep]}/sleep", method: 'POST', keys: keys)
|
||||
puts "#{GREEN}Service sleeping: #{options[:sleep]}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if options[:wake]
|
||||
api_request("/services/#{options[:wake]}/wake", method: 'POST', api_key: api_key)
|
||||
api_request("/services/#{options[:wake]}/wake", method: 'POST', keys: keys)
|
||||
puts "#{GREEN}Service waking: #{options[:wake]}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if options[:destroy]
|
||||
api_request("/services/#{options[:destroy]}", method: 'DELETE', api_key: api_key)
|
||||
api_request("/services/#{options[:destroy]}", method: 'DELETE', keys: keys)
|
||||
puts "#{GREEN}Service destroyed: #{options[:destroy]}#{RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
if options[:execute]
|
||||
payload = { command: options[:command] }
|
||||
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, api_key: api_key)
|
||||
result = api_request("/services/#{options[:execute]}/execute", method: 'POST', data: payload, keys: keys)
|
||||
print "#{BLUE}#{result['stdout']}#{RESET}" if result['stdout']
|
||||
$stderr.print "#{RED}#{result['stderr']}#{RESET}" if result['stderr']
|
||||
return
|
||||
|
|
@ -394,7 +422,7 @@ def cmd_service(options)
|
|||
if options[:dump_bootstrap]
|
||||
warn "Fetching bootstrap script from #{options[:dump_bootstrap]}..."
|
||||
payload = { command: 'cat /tmp/bootstrap.sh' }
|
||||
result = api_request("/services/#{options[:dump_bootstrap]}/execute", method: 'POST', data: payload, api_key: api_key)
|
||||
result = api_request("/services/#{options[:dump_bootstrap]}/execute", method: 'POST', data: payload, keys: keys)
|
||||
|
||||
if result['stdout']
|
||||
bootstrap = result['stdout']
|
||||
|
|
@ -434,7 +462,7 @@ def cmd_service(options)
|
|||
payload[:network] = options[:network] if options[:network]
|
||||
payload[:vcpu] = options[:vcpu] if options[:vcpu]
|
||||
|
||||
result = api_request('/services', method: 'POST', data: payload, api_key: api_key)
|
||||
result = api_request('/services', method: 'POST', data: payload, keys: keys)
|
||||
puts "#{GREEN}Service created: #{result['id'] || 'N/A'}#{RESET}"
|
||||
puts "Name: #{result['name'] || 'N/A'}"
|
||||
puts "URL: #{result['url']}" if result['url']
|
||||
|
|
|
|||
118
un.rs
118
un.rs
|
|
@ -49,6 +49,7 @@ use std::fs;
|
|||
use std::path::Path;
|
||||
use std::process::{self, Command};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const API_BASE: &str = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE: &str = "https://unsandbox.com";
|
||||
|
|
@ -109,14 +110,38 @@ fn detect_language(filename: &str) -> Option<&'static str> {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_api_key(key_arg: Option<&str>) -> String {
|
||||
if let Some(k) = key_arg {
|
||||
return k.to_string();
|
||||
fn get_api_keys(key_arg: Option<&str>) -> (String, String) {
|
||||
let public_key = env::var("UNSANDBOX_PUBLIC_KEY").ok();
|
||||
let secret_key = env::var("UNSANDBOX_SECRET_KEY").ok();
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if public_key.is_none() || secret_key.is_none() {
|
||||
let fallback_key = if let Some(k) = key_arg {
|
||||
k.to_string()
|
||||
} else {
|
||||
env::var("UNSANDBOX_API_KEY").unwrap_or_else(|_| {
|
||||
eprintln!("{}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat){}", RED, RESET);
|
||||
process::exit(1);
|
||||
})
|
||||
};
|
||||
return (fallback_key.clone(), fallback_key);
|
||||
}
|
||||
env::var("UNSANDBOX_API_KEY").unwrap_or_else(|_| {
|
||||
eprintln!("{}Error: UNSANDBOX_API_KEY not set{}", RED, RESET);
|
||||
process::exit(1);
|
||||
})
|
||||
|
||||
(public_key.unwrap(), secret_key.unwrap())
|
||||
}
|
||||
|
||||
fn compute_hmac(secret_key: &str, timestamp: &str, method: &str, path: &str, body: &str) -> String {
|
||||
use std::process::Command;
|
||||
let message = format!("{}:{}:{}:{}", timestamp, method, path, body);
|
||||
|
||||
// Use openssl for HMAC-SHA256
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("printf '%s' '{}' | openssl dgst -sha256 -hmac '{}' | cut -d' ' -f2", message, secret_key))
|
||||
.output()
|
||||
.expect("Failed to compute HMAC");
|
||||
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
}
|
||||
|
||||
fn escape_json(s: &str) -> String {
|
||||
|
|
@ -163,8 +188,18 @@ fn extract_json_int(json: &str, key: &str) -> i32 {
|
|||
1
|
||||
}
|
||||
|
||||
fn api_request(endpoint: &str, method: &str, body: Option<&str>, api_key: &str) -> String {
|
||||
fn api_request(endpoint: &str, method: &str, body: Option<&str>, public_key: &str, secret_key: &str) -> String {
|
||||
let url = format!("{}{}", API_BASE, endpoint);
|
||||
let body_str = body.unwrap_or("");
|
||||
|
||||
// Compute HMAC signature
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
.to_string();
|
||||
let signature = compute_hmac(secret_key, ×tamp, method, endpoint, body_str);
|
||||
|
||||
let mut cmd = Command::new("curl");
|
||||
cmd.arg("-s")
|
||||
.arg("-X")
|
||||
|
|
@ -173,7 +208,11 @@ fn api_request(endpoint: &str, method: &str, body: Option<&str>, api_key: &str)
|
|||
.arg("-H")
|
||||
.arg("Content-Type: application/json")
|
||||
.arg("-H")
|
||||
.arg(format!("Authorization: Bearer {}", api_key));
|
||||
.arg(format!("Authorization: Bearer {}", public_key))
|
||||
.arg("-H")
|
||||
.arg(format!("X-Timestamp: {}", timestamp))
|
||||
.arg("-H")
|
||||
.arg(format!("X-Signature: {}", signature));
|
||||
|
||||
if let Some(b) = body {
|
||||
cmd.arg("-d").arg(b);
|
||||
|
|
@ -200,7 +239,8 @@ fn cmd_execute(
|
|||
output_dir: Option<&str>,
|
||||
network: Option<&str>,
|
||||
vcpu: Option<i32>,
|
||||
api_key: &str,
|
||||
public_key: &str,
|
||||
secret_key: &str,
|
||||
) {
|
||||
let code = fs::read_to_string(source_file).unwrap_or_else(|e| {
|
||||
eprintln!("{}Error reading file: {}{}", RED, e, RESET);
|
||||
|
|
@ -265,7 +305,7 @@ fn cmd_execute(
|
|||
|
||||
json.push('}');
|
||||
|
||||
let result = api_request("/execute", "POST", Some(&json), api_key);
|
||||
let result = api_request("/execute", "POST", Some(&json), public_key, secret_key);
|
||||
|
||||
// Print output
|
||||
let stdout_str = extract_json_string(&result, "stdout");
|
||||
|
|
@ -295,16 +335,17 @@ fn cmd_session(
|
|||
vcpu: Option<i32>,
|
||||
tmux: bool,
|
||||
screen: bool,
|
||||
api_key: &str,
|
||||
public_key: &str,
|
||||
secret_key: &str,
|
||||
) {
|
||||
if list {
|
||||
let result = api_request("/sessions", "GET", None, api_key);
|
||||
let result = api_request("/sessions", "GET", None, public_key, secret_key);
|
||||
println!("{}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = kill {
|
||||
api_request(&format!("/sessions/{}", id), "DELETE", None, api_key);
|
||||
api_request(&format!("/sessions/{}", id), "DELETE", None, public_key, secret_key);
|
||||
println!("{}Session terminated: {}{}", GREEN, id, RESET);
|
||||
return;
|
||||
}
|
||||
|
|
@ -330,7 +371,7 @@ fn cmd_session(
|
|||
json.push('}');
|
||||
|
||||
println!("{}Creating session...{}", YELLOW, RESET);
|
||||
let result = api_request("/sessions", "POST", Some(&json), api_key);
|
||||
let result = api_request("/sessions", "POST", Some(&json), public_key, secret_key);
|
||||
let id = extract_json_string(&result, "id");
|
||||
println!("{}Session created: {}{}", GREEN, id, RESET);
|
||||
}
|
||||
|
|
@ -354,46 +395,47 @@ fn cmd_service(
|
|||
dump_file: Option<&str>,
|
||||
network: Option<&str>,
|
||||
vcpu: Option<i32>,
|
||||
api_key: &str,
|
||||
public_key: &str,
|
||||
secret_key: &str,
|
||||
) {
|
||||
if list {
|
||||
let result = api_request("/services", "GET", None, api_key);
|
||||
let result = api_request("/services", "GET", None, public_key, secret_key);
|
||||
println!("{}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = info {
|
||||
let result = api_request(&format!("/services/{}", id), "GET", None, api_key);
|
||||
let result = api_request(&format!("/services/{}", id), "GET", None, public_key, secret_key);
|
||||
println!("{}", result);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = logs {
|
||||
let result = api_request(&format!("/services/{}/logs", id), "GET", None, api_key);
|
||||
let result = api_request(&format!("/services/{}/logs", id), "GET", None, public_key, secret_key);
|
||||
println!("{}", extract_json_string(&result, "logs"));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = tail {
|
||||
let result = api_request(&format!("/services/{}/logs?lines=9000", id), "GET", None, api_key);
|
||||
let result = api_request(&format!("/services/{}/logs?lines=9000", id), "GET", None, public_key, secret_key);
|
||||
println!("{}", extract_json_string(&result, "logs"));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = sleep {
|
||||
api_request(&format!("/services/{}/sleep", id), "POST", None, api_key);
|
||||
api_request(&format!("/services/{}/sleep", id), "POST", None, public_key, secret_key);
|
||||
println!("{}Service sleeping: {}{}", GREEN, id, RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = wake {
|
||||
api_request(&format!("/services/{}/wake", id), "POST", None, api_key);
|
||||
api_request(&format!("/services/{}/wake", id), "POST", None, public_key, secret_key);
|
||||
println!("{}Service waking: {}{}", GREEN, id, RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = destroy {
|
||||
api_request(&format!("/services/{}", id), "DELETE", None, api_key);
|
||||
api_request(&format!("/services/{}", id), "DELETE", None, public_key, secret_key);
|
||||
println!("{}Service destroyed: {}{}", GREEN, id, RESET);
|
||||
return;
|
||||
}
|
||||
|
|
@ -401,7 +443,7 @@ fn cmd_service(
|
|||
if let Some(id) = execute {
|
||||
let cmd = command.unwrap_or("");
|
||||
let json = format!(r#"{{"command":"{}"}}"#, escape_json(cmd));
|
||||
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(&json), api_key);
|
||||
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(&json), public_key, secret_key);
|
||||
let stdout_str = extract_json_string(&result, "stdout");
|
||||
let stderr_str = extract_json_string(&result, "stderr");
|
||||
if !stdout_str.is_empty() {
|
||||
|
|
@ -416,7 +458,7 @@ fn cmd_service(
|
|||
if let Some(id) = dump_bootstrap {
|
||||
eprintln!("Fetching bootstrap script from {}...", id);
|
||||
let json = r#"{"command":"cat /tmp/bootstrap.sh"}"#;
|
||||
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(json), api_key);
|
||||
let result = api_request(&format!("/services/{}/execute", id), "POST", Some(json), public_key, secret_key);
|
||||
let bootstrap = extract_json_string(&result, "stdout");
|
||||
|
||||
if !bootstrap.is_empty() {
|
||||
|
|
@ -495,7 +537,7 @@ fn cmd_service(
|
|||
|
||||
json.push('}');
|
||||
|
||||
let result = api_request("/services", "POST", Some(&json), api_key);
|
||||
let result = api_request("/services", "POST", Some(&json), public_key, secret_key);
|
||||
let id = extract_json_string(&result, "id");
|
||||
println!("{}Service created: {}{}", GREEN, id, RESET);
|
||||
return;
|
||||
|
|
@ -505,8 +547,8 @@ fn cmd_service(
|
|||
process::exit(1);
|
||||
}
|
||||
|
||||
fn cmd_key(extend: bool, api_key: &str) {
|
||||
let result = api_request("/keys/validate", "POST", Some("{}"), api_key);
|
||||
fn cmd_key(extend: bool, public_key: &str, secret_key: &str) {
|
||||
let result = api_request("/keys/validate", "POST", Some("{}"), public_key, secret_key);
|
||||
|
||||
let status = extract_json_string(&result, "status");
|
||||
let public_key = extract_json_string(&result, "public_key");
|
||||
|
|
@ -647,7 +689,7 @@ fn main() {
|
|||
}
|
||||
}
|
||||
"session" => {
|
||||
let key = get_api_key(api_key.as_deref());
|
||||
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
|
||||
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()),
|
||||
|
|
@ -656,12 +698,13 @@ fn main() {
|
|||
vcpu,
|
||||
args.contains(&"--tmux".to_string()),
|
||||
args.contains(&"--screen".to_string()),
|
||||
&key,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
"service" => {
|
||||
let key = get_api_key(api_key.as_deref());
|
||||
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
|
||||
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()),
|
||||
|
|
@ -681,15 +724,17 @@ fn main() {
|
|||
args.iter().position(|x| x == "--dump-file").and_then(|p| args.get(p + 1)).map(|s| s.as_str()),
|
||||
network.as_deref(),
|
||||
vcpu,
|
||||
&key,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
"key" => {
|
||||
let key = get_api_key(api_key.as_deref());
|
||||
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
|
||||
cmd_key(
|
||||
args.contains(&"--extend".to_string()),
|
||||
&key,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -704,7 +749,7 @@ fn main() {
|
|||
|
||||
// Execute mode
|
||||
if let Some(file) = source_file {
|
||||
let key = get_api_key(api_key.as_deref());
|
||||
let (public_key, secret_key) = get_api_keys(api_key.as_deref());
|
||||
cmd_execute(
|
||||
&file,
|
||||
envs,
|
||||
|
|
@ -713,7 +758,8 @@ fn main() {
|
|||
output_dir.as_deref(),
|
||||
network.as_deref(),
|
||||
vcpu,
|
||||
&key,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
);
|
||||
} else {
|
||||
eprintln!("{}Error: No source file specified{}", RED, RESET);
|
||||
|
|
|
|||
75
un.scm
75
un.scm
|
|
@ -101,8 +101,14 @@
|
|||
|
||||
(define (curl-post api-key endpoint json-data)
|
||||
(let* ((tmp-file (write-temp-file json-data))
|
||||
(cmd (format #f "curl -s -X POST https://api.unsandbox.com~a -H 'Content-Type: application/json' -H 'Authorization: Bearer ~a' -d @~a"
|
||||
endpoint api-key tmp-file))
|
||||
(keys (get-api-keys))
|
||||
(public-key (car keys))
|
||||
(secret-key (cadr keys))
|
||||
(auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
|
||||
(cmd (string-append "curl -s -X POST https://api.unsandbox.com" endpoint
|
||||
" -H 'Content-Type: application/json' "
|
||||
(string-join auth-headers " ")
|
||||
" -d @" tmp-file))
|
||||
(port (open-input-pipe cmd))
|
||||
(output (let loop ((lines '()))
|
||||
(let ((line (read-line port)))
|
||||
|
|
@ -114,8 +120,12 @@
|
|||
output))
|
||||
|
||||
(define (curl-get api-key endpoint)
|
||||
(let* ((cmd (format #f "curl -s https://api.unsandbox.com~a -H 'Authorization: Bearer ~a'"
|
||||
endpoint api-key))
|
||||
(let* ((keys (get-api-keys))
|
||||
(public-key (car keys))
|
||||
(secret-key (cadr keys))
|
||||
(auth-headers (build-auth-headers public-key secret-key "GET" endpoint ""))
|
||||
(cmd (string-append "curl -s https://api.unsandbox.com" endpoint
|
||||
" " (string-join auth-headers " ")))
|
||||
(port (open-input-pipe cmd))
|
||||
(output (let loop ((lines '()))
|
||||
(let ((line (read-line port)))
|
||||
|
|
@ -126,8 +136,12 @@
|
|||
output))
|
||||
|
||||
(define (curl-delete api-key endpoint)
|
||||
(let* ((cmd (format #f "curl -s -X DELETE https://api.unsandbox.com~a -H 'Authorization: Bearer ~a'"
|
||||
endpoint api-key))
|
||||
(let* ((keys (get-api-keys))
|
||||
(public-key (car keys))
|
||||
(secret-key (cadr keys))
|
||||
(auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint ""))
|
||||
(cmd (string-append "curl -s -X DELETE https://api.unsandbox.com" endpoint
|
||||
" " (string-join auth-headers " ")))
|
||||
(port (open-input-pipe cmd))
|
||||
(output (let loop ((lines '()))
|
||||
(let ((line (read-line port)))
|
||||
|
|
@ -139,8 +153,14 @@
|
|||
|
||||
(define (curl-post-portal api-key endpoint json-data)
|
||||
(let* ((tmp-file (write-temp-file json-data))
|
||||
(cmd (format #f "curl -s -X POST ~a~a -H 'Content-Type: application/json' -H 'Authorization: Bearer ~a' -d @~a"
|
||||
portal-base endpoint api-key tmp-file))
|
||||
(keys (get-api-keys))
|
||||
(public-key (car keys))
|
||||
(secret-key (cadr keys))
|
||||
(auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data))
|
||||
(cmd (string-append "curl -s -X POST " portal-base endpoint
|
||||
" -H 'Content-Type: application/json' "
|
||||
(string-join auth-headers " ")
|
||||
" -d @" tmp-file))
|
||||
(port (open-input-pipe cmd))
|
||||
(output (let loop ((lines '()))
|
||||
(let ((line (read-line port)))
|
||||
|
|
@ -151,11 +171,42 @@
|
|||
(delete-file tmp-file)
|
||||
output))
|
||||
|
||||
(define (get-api-keys)
|
||||
(let ((public-key (getenv "UNSANDBOX_PUBLIC_KEY"))
|
||||
(secret-key (getenv "UNSANDBOX_SECRET_KEY"))
|
||||
(api-key (getenv "UNSANDBOX_API_KEY")))
|
||||
(cond
|
||||
((and public-key secret-key) (list public-key secret-key))
|
||||
(api-key (list api-key #f))
|
||||
(else (begin
|
||||
(display "Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set (or UNSANDBOX_API_KEY for backwards compat)\n" (current-error-port))
|
||||
(exit 1))))))
|
||||
|
||||
(define (get-api-key)
|
||||
(or (getenv "UNSANDBOX_API_KEY")
|
||||
(begin
|
||||
(display "Error: UNSANDBOX_API_KEY not set\n" (current-error-port))
|
||||
(exit 1))))
|
||||
(car (get-api-keys)))
|
||||
|
||||
(define (hmac-sha256 secret message)
|
||||
"Compute HMAC-SHA256 using openssl command"
|
||||
(let* ((cmd (format #f "echo -n '~a' | openssl dgst -sha256 -hmac '~a' | awk '{print $2}'"
|
||||
(string-append (list->string (map (lambda (c) (if (char=? c #\') #\space c)) (string->list message))))
|
||||
(string-append (list->string (map (lambda (c) (if (char=? c #\') #\space c)) (string->list secret))))))
|
||||
(port (open-input-pipe cmd))
|
||||
(result (read-line port)))
|
||||
(close-pipe port)
|
||||
(string-trim-both result)))
|
||||
|
||||
(define (make-signature secret-key timestamp method path body)
|
||||
(let ((message (format #f "~a:~a:~a:~a" timestamp method path body)))
|
||||
(hmac-sha256 secret-key message)))
|
||||
|
||||
(define (build-auth-headers public-key secret-key method path body)
|
||||
(if secret-key
|
||||
(let* ((timestamp (number->string (quotient (current-time) 1)))
|
||||
(signature (make-signature secret-key timestamp method path body)))
|
||||
(list "-H" (format #f "Authorization: Bearer ~a" public-key)
|
||||
"-H" (format #f "X-Timestamp: ~a" timestamp)
|
||||
"-H" (format #f "X-Signature: ~a" signature)))
|
||||
(list "-H" (format #f "Authorization: Bearer ~a" public-key))))
|
||||
|
||||
(define (json-extract-string json key)
|
||||
"Extract string value for key from JSON (simple parser)"
|
||||
|
|
|
|||
63
un.sh
Normal file → Executable file
63
un.sh
Normal file → Executable file
|
|
@ -130,27 +130,60 @@ api_request() {
|
|||
local endpoint="$1"
|
||||
local method="${2:-GET}"
|
||||
local data="${3:-}"
|
||||
local api_key="${4:-${UNSANDBOX_API_KEY}}"
|
||||
local public_key="${4:-${UNSANDBOX_PUBLIC_KEY:-}}"
|
||||
local secret_key="${5:-${UNSANDBOX_SECRET_KEY:-}}"
|
||||
|
||||
if [[ -z "$api_key" ]]; then
|
||||
echo -e "${RED}Error: UNSANDBOX_API_KEY not set${RESET}" >&2
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if [[ -z "$public_key" ]] && [[ -n "${UNSANDBOX_API_KEY:-}" ]]; then
|
||||
public_key="${UNSANDBOX_API_KEY}"
|
||||
secret_key=""
|
||||
fi
|
||||
|
||||
if [[ -z "$public_key" ]]; then
|
||||
echo -e "${RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${RESET}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local url="${API_BASE}${endpoint}"
|
||||
local tmpfile=$(mktemp)
|
||||
local timestamp=$(date +%s)
|
||||
local body="${data:-}"
|
||||
|
||||
# Build HMAC signature: timestamp:METHOD:path:body
|
||||
local sig_input="${timestamp}:${method}:${endpoint}:${body}"
|
||||
local signature=""
|
||||
|
||||
if [[ -n "$secret_key" ]]; then
|
||||
signature=$(echo -n "$sig_input" | openssl dgst -sha256 -hmac "$secret_key" | sed 's/^.* //')
|
||||
fi
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" 2>&1)
|
||||
if [[ -n "$signature" ]]; then
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $public_key" \
|
||||
-H "X-Timestamp: $timestamp" \
|
||||
-H "X-Signature: $signature" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" 2>&1)
|
||||
else
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $public_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$data" 2>&1)
|
||||
fi
|
||||
else
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $api_key" \
|
||||
-H "Content-Type: application/json" 2>&1)
|
||||
if [[ -n "$signature" ]]; then
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $public_key" \
|
||||
-H "X-Timestamp: $timestamp" \
|
||||
-H "X-Signature: $signature" \
|
||||
-H "Content-Type: application/json" 2>&1)
|
||||
else
|
||||
response=$(curl -s -w "\n%{http_code}" -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $public_key" \
|
||||
-H "Content-Type: application/json" 2>&1)
|
||||
fi
|
||||
fi
|
||||
|
||||
local http_code=$(echo "$response" | tail -n1)
|
||||
|
|
@ -172,7 +205,7 @@ cmd_execute() {
|
|||
local output_dir="."
|
||||
local network=""
|
||||
local vcpu=""
|
||||
local api_key="${UNSANDBOX_API_KEY}"
|
||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
|
@ -300,7 +333,7 @@ cmd_session() {
|
|||
local screen=false
|
||||
local network=""
|
||||
local vcpu=""
|
||||
local api_key="${UNSANDBOX_API_KEY}"
|
||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -409,7 +442,7 @@ cmd_service() {
|
|||
local command=""
|
||||
local network=""
|
||||
local vcpu=""
|
||||
local api_key="${UNSANDBOX_API_KEY}"
|
||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -743,7 +776,7 @@ validate_key() {
|
|||
}
|
||||
|
||||
cmd_key() {
|
||||
local api_key="${UNSANDBOX_API_KEY}"
|
||||
local api_key="${UNSANDBOX_API_KEY:-}"
|
||||
local extend=false
|
||||
|
||||
# Parse arguments
|
||||
|
|
|
|||
92
un.tcl
Normal file → Executable file
92
un.tcl
Normal file → Executable file
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env tclsh
|
||||
# PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
||||
#
|
||||
# This is free public domain software for the public good of a permacomputer hosted
|
||||
|
|
@ -34,8 +35,6 @@
|
|||
# https://www.foxhop.net
|
||||
# https://www.unturf.com/software
|
||||
|
||||
#!/usr/bin/env tclsh
|
||||
|
||||
# unsandbox CLI - TCL implementation
|
||||
# Full-featured CLI matching un.c/un.py capabilities
|
||||
|
||||
|
|
@ -43,6 +42,7 @@ package require http
|
|||
package require json
|
||||
package require tls
|
||||
package require base64
|
||||
package require sha256
|
||||
|
||||
# Register https support
|
||||
::http::register https 443 ::tls::socket
|
||||
|
|
@ -72,12 +72,28 @@ array set EXT_MAP {
|
|||
.tcl tcl .raku raku .m objc
|
||||
}
|
||||
|
||||
proc get_api_key {} {
|
||||
if {[info exists ::env(UNSANDBOX_API_KEY)]} {
|
||||
return $::env(UNSANDBOX_API_KEY)
|
||||
proc get_api_keys {} {
|
||||
set public_key ""
|
||||
set secret_key ""
|
||||
|
||||
if {[info exists ::env(UNSANDBOX_PUBLIC_KEY)]} {
|
||||
set public_key $::env(UNSANDBOX_PUBLIC_KEY)
|
||||
}
|
||||
puts stderr "${::RED}Error: UNSANDBOX_API_KEY not set${::RESET}"
|
||||
exit 1
|
||||
if {[info exists ::env(UNSANDBOX_SECRET_KEY)]} {
|
||||
set secret_key $::env(UNSANDBOX_SECRET_KEY)
|
||||
}
|
||||
|
||||
# Fallback to old UNSANDBOX_API_KEY for backwards compat
|
||||
if {$public_key eq "" && [info exists ::env(UNSANDBOX_API_KEY)]} {
|
||||
set public_key $::env(UNSANDBOX_API_KEY)
|
||||
set secret_key ""
|
||||
}
|
||||
|
||||
if {$public_key eq ""} {
|
||||
puts stderr "${::RED}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY not set${::RESET}"
|
||||
exit 1
|
||||
}
|
||||
return [list $public_key $secret_key]
|
||||
}
|
||||
|
||||
proc detect_language {filename} {
|
||||
|
|
@ -103,16 +119,29 @@ proc detect_language {filename} {
|
|||
exit 1
|
||||
}
|
||||
|
||||
proc api_request {endpoint method data api_key} {
|
||||
proc api_request {endpoint method data public_key secret_key} {
|
||||
set url "${::API_BASE}${endpoint}"
|
||||
set headers [list Authorization "Bearer $api_key" Content-Type "application/json"]
|
||||
set headers [list Authorization "Bearer $public_key" Content-Type "application/json"]
|
||||
|
||||
set json_data ""
|
||||
if {$method ne "GET" && $method ne "DELETE" && [llength $data] > 0} {
|
||||
set json_data [::json::write object {*}$data]
|
||||
}
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if {$secret_key ne ""} {
|
||||
set timestamp [clock seconds]
|
||||
set sig_input "${timestamp}:${method}:${endpoint}:${json_data}"
|
||||
set signature [::sha2::hmac -hex -key $secret_key $sig_input]
|
||||
lappend headers X-Timestamp $timestamp
|
||||
lappend headers X-Signature $signature
|
||||
}
|
||||
|
||||
if {$method eq "GET"} {
|
||||
set token [::http::geturl $url -headers $headers -timeout 300000]
|
||||
} elseif {$method eq "DELETE"} {
|
||||
set token [::http::geturl $url -method DELETE -headers $headers -timeout 300000]
|
||||
} else {
|
||||
set json_data [::json::write object {*}$data]
|
||||
set token [::http::geturl $url -method $method -headers $headers -query $json_data -timeout 300000]
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +160,7 @@ proc api_request {endpoint method data api_key} {
|
|||
}
|
||||
|
||||
proc cmd_execute {args} {
|
||||
set api_key [get_api_key]
|
||||
lassign [get_api_keys] public_key secret_key
|
||||
set source_file ""
|
||||
set env_vars [dict create]
|
||||
set input_files [list]
|
||||
|
|
@ -236,7 +265,7 @@ proc cmd_execute {args} {
|
|||
}
|
||||
|
||||
# Execute
|
||||
set result [api_request "/execute" "POST" $payload $api_key]
|
||||
set result [api_request "/execute" "POST" $payload $public_key $secret_key]
|
||||
|
||||
# Print output
|
||||
if {[dict exists $result stdout]} {
|
||||
|
|
@ -275,7 +304,7 @@ proc cmd_execute {args} {
|
|||
}
|
||||
|
||||
proc cmd_session {args} {
|
||||
set api_key [get_api_key]
|
||||
lassign [get_api_keys] public_key secret_key
|
||||
set list_mode 0
|
||||
set kill_id ""
|
||||
set shell ""
|
||||
|
|
@ -309,7 +338,7 @@ proc cmd_session {args} {
|
|||
}
|
||||
|
||||
if {$list_mode} {
|
||||
set result [api_request "/sessions" "GET" {} $api_key]
|
||||
set result [api_request "/sessions" "GET" {} $public_key $secret_key]
|
||||
set sessions [dict get $result sessions]
|
||||
if {[llength $sessions] == 0} {
|
||||
puts "No active sessions"
|
||||
|
|
@ -327,7 +356,7 @@ proc cmd_session {args} {
|
|||
}
|
||||
|
||||
if {$kill_id ne ""} {
|
||||
api_request "/sessions/$kill_id" "DELETE" {} $api_key
|
||||
api_request "/sessions/$kill_id" "DELETE" {} $public_key $secret_key
|
||||
puts "${::GREEN}Session terminated: $kill_id${::RESET}"
|
||||
return
|
||||
}
|
||||
|
|
@ -347,13 +376,13 @@ proc cmd_session {args} {
|
|||
}
|
||||
|
||||
puts "${::YELLOW}Creating session...${::RESET}"
|
||||
set result [api_request "/sessions" "POST" $payload $api_key]
|
||||
set result [api_request "/sessions" "POST" $payload $public_key $secret_key]
|
||||
puts "${::GREEN}Session created: [dict get $result id]${::RESET}"
|
||||
puts "${::YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${::RESET}"
|
||||
}
|
||||
|
||||
proc cmd_key {args} {
|
||||
set api_key [get_api_key]
|
||||
lassign [get_api_keys] public_key secret_key
|
||||
set extend_mode 0
|
||||
|
||||
# Parse arguments
|
||||
|
|
@ -368,7 +397,16 @@ proc cmd_key {args} {
|
|||
|
||||
# POST to /keys/validate with Bearer auth
|
||||
set url "${::PORTAL_BASE}/keys/validate"
|
||||
set headers [list Authorization "Bearer $api_key" Content-Type "application/json"]
|
||||
set headers [list Authorization "Bearer $public_key" Content-Type "application/json"]
|
||||
|
||||
# Add HMAC signature if secret_key is present
|
||||
if {$secret_key ne ""} {
|
||||
set timestamp [clock seconds]
|
||||
set sig_input "${timestamp}:POST:/keys/validate:"
|
||||
set signature [::sha2::hmac -hex -key $secret_key $sig_input]
|
||||
lappend headers X-Timestamp $timestamp
|
||||
lappend headers X-Signature $signature
|
||||
}
|
||||
|
||||
set token [::http::geturl $url -method POST -headers $headers -timeout 30000]
|
||||
set status [::http::status $token]
|
||||
|
|
@ -438,7 +476,7 @@ proc cmd_key {args} {
|
|||
}
|
||||
|
||||
proc cmd_service {args} {
|
||||
set api_key [get_api_key]
|
||||
lassign [get_api_keys] public_key secret_key
|
||||
set list_mode 0
|
||||
set info_id ""
|
||||
set logs_id ""
|
||||
|
|
@ -517,7 +555,7 @@ proc cmd_service {args} {
|
|||
}
|
||||
|
||||
if {$list_mode} {
|
||||
set result [api_request "/services" "GET" {} $api_key]
|
||||
set result [api_request "/services" "GET" {} $public_key $secret_key]
|
||||
set services [dict get $result services]
|
||||
if {[llength $services] == 0} {
|
||||
puts "No services"
|
||||
|
|
@ -538,31 +576,31 @@ proc cmd_service {args} {
|
|||
}
|
||||
|
||||
if {$info_id ne ""} {
|
||||
set result [api_request "/services/$info_id" "GET" {} $api_key]
|
||||
set result [api_request "/services/$info_id" "GET" {} $public_key $secret_key]
|
||||
puts [::json::write object {*}[dict_to_json_list $result]]
|
||||
return
|
||||
}
|
||||
|
||||
if {$logs_id ne ""} {
|
||||
set result [api_request "/services/$logs_id/logs" "GET" {} $api_key]
|
||||
set result [api_request "/services/$logs_id/logs" "GET" {} $public_key $secret_key]
|
||||
puts [dict get $result logs]
|
||||
return
|
||||
}
|
||||
|
||||
if {$sleep_id ne ""} {
|
||||
api_request "/services/$sleep_id/sleep" "POST" {} $api_key
|
||||
api_request "/services/$sleep_id/sleep" "POST" {} $public_key $secret_key
|
||||
puts "${::GREEN}Service sleeping: $sleep_id${::RESET}"
|
||||
return
|
||||
}
|
||||
|
||||
if {$wake_id ne ""} {
|
||||
api_request "/services/$wake_id/wake" "POST" {} $api_key
|
||||
api_request "/services/$wake_id/wake" "POST" {} $public_key $secret_key
|
||||
puts "${::GREEN}Service waking: $wake_id${::RESET}"
|
||||
return
|
||||
}
|
||||
|
||||
if {$destroy_id ne ""} {
|
||||
api_request "/services/$destroy_id" "DELETE" {} $api_key
|
||||
api_request "/services/$destroy_id" "DELETE" {} $public_key $secret_key
|
||||
puts "${::GREEN}Service destroyed: $destroy_id${::RESET}"
|
||||
return
|
||||
}
|
||||
|
|
@ -570,7 +608,7 @@ proc cmd_service {args} {
|
|||
if {$dump_bootstrap_id ne ""} {
|
||||
puts stderr "Fetching bootstrap script from $dump_bootstrap_id..."
|
||||
set payload [list command [::json::write string "cat /tmp/bootstrap.sh"]]
|
||||
set result [api_request "/services/$dump_bootstrap_id/execute" "POST" $payload $api_key]
|
||||
set result [api_request "/services/$dump_bootstrap_id/execute" "POST" $payload $public_key $secret_key]
|
||||
|
||||
if {[dict exists $result stdout] && [dict get $result stdout] ne ""} {
|
||||
set bootstrap [dict get $result stdout]
|
||||
|
|
@ -628,7 +666,7 @@ proc cmd_service {args} {
|
|||
lappend payload vcpu $vcpu
|
||||
}
|
||||
|
||||
set result [api_request "/services" "POST" $payload $api_key]
|
||||
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]"
|
||||
if {[dict exists $result url]} {
|
||||
|
|
|
|||
96
un.ts
96
un.ts
|
|
@ -54,6 +54,7 @@
|
|||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
|
|
@ -112,13 +113,28 @@ interface Args {
|
|||
extend: boolean;
|
||||
}
|
||||
|
||||
function getApiKey(argsKey: string | null): string {
|
||||
const key = argsKey || process.env.UNSANDBOX_API_KEY;
|
||||
if (!key) {
|
||||
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`);
|
||||
process.exit(1);
|
||||
interface ApiKeys {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
}
|
||||
|
||||
function getApiKeys(argsKey: string | null): ApiKeys {
|
||||
let publicKey = process.env.UNSANDBOX_PUBLIC_KEY;
|
||||
let secretKey = process.env.UNSANDBOX_SECRET_KEY;
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
const oldKey = argsKey || process.env.UNSANDBOX_API_KEY;
|
||||
if (oldKey) {
|
||||
publicKey = oldKey;
|
||||
secretKey = oldKey;
|
||||
} else {
|
||||
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
|
||||
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return key;
|
||||
|
||||
return { publicKey, secretKey };
|
||||
}
|
||||
|
||||
function detectLanguage(filename: string): string {
|
||||
|
|
@ -143,15 +159,22 @@ function detectLanguage(filename: string): string {
|
|||
return lang;
|
||||
}
|
||||
|
||||
function apiRequest(endpoint: string, method: string = "GET", data: any = null, apiKey: string): Promise<any> {
|
||||
function apiRequest(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(API_BASE + endpoint);
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const body = data ? JSON.stringify(data) : '';
|
||||
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
|
||||
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Authorization': `Bearer ${keys.publicKey}`,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Signature': signature,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 300000
|
||||
|
|
@ -180,21 +203,28 @@ function apiRequest(endpoint: string, method: string = "GET", data: any = null,
|
|||
});
|
||||
|
||||
if (data) {
|
||||
req.write(JSON.stringify(data));
|
||||
req.write(body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function portalRequest(endpoint: string, method: string = "GET", data: any = null, apiKey: string): Promise<any> {
|
||||
function portalRequest(endpoint: string, method: string = "GET", data: any = null, keys: ApiKeys): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(PORTAL_BASE + endpoint);
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const body = data ? JSON.stringify(data) : '';
|
||||
const message = `${timestamp}:${method}:${url.pathname}${url.search}:${body}`;
|
||||
const signature = crypto.createHmac('sha256', keys.secretKey).update(message).digest('hex');
|
||||
|
||||
const options: https.RequestOptions = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Authorization': `Bearer ${keys.publicKey}`,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Signature': signature,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 30000
|
||||
|
|
@ -218,14 +248,14 @@ function portalRequest(endpoint: string, method: string = "GET", data: any = nul
|
|||
});
|
||||
|
||||
if (data) {
|
||||
req.write(JSON.stringify(data));
|
||||
req.write(body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function cmdExecute(args: Args): Promise<void> {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
const keys = getApiKeys(args.apiKey);
|
||||
|
||||
let code: string;
|
||||
try {
|
||||
|
|
@ -267,7 +297,7 @@ async function cmdExecute(args: Args): Promise<void> {
|
|||
if (args.network) payload.network = args.network;
|
||||
if (args.vcpu) payload.vcpu = args.vcpu;
|
||||
|
||||
const result = await apiRequest("/execute", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/execute", "POST", payload, keys);
|
||||
|
||||
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
|
||||
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
|
||||
|
|
@ -289,10 +319,10 @@ async function cmdExecute(args: Args): Promise<void> {
|
|||
}
|
||||
|
||||
async function cmdSession(args: Args): Promise<void> {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
const keys = getApiKeys(args.apiKey);
|
||||
|
||||
if (args.list) {
|
||||
const result = await apiRequest("/sessions", "GET", null, apiKey);
|
||||
const result = await apiRequest("/sessions", "GET", null, keys);
|
||||
const sessions = result.sessions || [];
|
||||
if (sessions.length === 0) {
|
||||
console.log("No active sessions");
|
||||
|
|
@ -306,7 +336,7 @@ async function cmdSession(args: Args): Promise<void> {
|
|||
}
|
||||
|
||||
if (args.kill) {
|
||||
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, apiKey);
|
||||
await apiRequest(`/sessions/${args.kill}`, "DELETE", null, keys);
|
||||
console.log(`${GREEN}Session terminated: ${args.kill}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -325,16 +355,16 @@ async function cmdSession(args: Args): Promise<void> {
|
|||
if (args.audit) payload.audit = true;
|
||||
|
||||
console.log(`${YELLOW}Creating session...${RESET}`);
|
||||
const result = await apiRequest("/sessions", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/sessions", "POST", payload, keys);
|
||||
console.log(`${GREEN}Session created: ${result.id || 'N/A'}${RESET}`);
|
||||
console.log(`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`);
|
||||
}
|
||||
|
||||
async function cmdService(args: Args): Promise<void> {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
const keys = getApiKeys(args.apiKey);
|
||||
|
||||
if (args.list) {
|
||||
const result = await apiRequest("/services", "GET", null, apiKey);
|
||||
const result = await apiRequest("/services", "GET", null, keys);
|
||||
const services = result.services || [];
|
||||
if (services.length === 0) {
|
||||
console.log("No services");
|
||||
|
|
@ -350,44 +380,44 @@ async function cmdService(args: Args): Promise<void> {
|
|||
}
|
||||
|
||||
if (args.info) {
|
||||
const result = await apiRequest(`/services/${args.info}`, "GET", null, apiKey);
|
||||
const result = await apiRequest(`/services/${args.info}`, "GET", null, keys);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.logs) {
|
||||
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, apiKey);
|
||||
const result = await apiRequest(`/services/${args.logs}/logs`, "GET", null, keys);
|
||||
console.log(result.logs || "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.tail) {
|
||||
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, apiKey);
|
||||
const result = await apiRequest(`/services/${args.tail}/logs?lines=9000`, "GET", null, keys);
|
||||
console.log(result.logs || "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.sleep) {
|
||||
await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, apiKey);
|
||||
await apiRequest(`/services/${args.sleep}/sleep`, "POST", null, keys);
|
||||
console.log(`${GREEN}Service sleeping: ${args.sleep}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.wake) {
|
||||
await apiRequest(`/services/${args.wake}/wake`, "POST", null, apiKey);
|
||||
await apiRequest(`/services/${args.wake}/wake`, "POST", null, keys);
|
||||
console.log(`${GREEN}Service waking: ${args.wake}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.destroy) {
|
||||
await apiRequest(`/services/${args.destroy}`, "DELETE", null, apiKey);
|
||||
await apiRequest(`/services/${args.destroy}`, "DELETE", null, keys);
|
||||
console.log(`${GREEN}Service destroyed: ${args.destroy}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.execute) {
|
||||
const payload = { command: args.command_arg };
|
||||
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, apiKey);
|
||||
const result = await apiRequest(`/services/${args.execute}/execute`, "POST", payload, keys);
|
||||
if (result.stdout) process.stdout.write(`${BLUE}${result.stdout}${RESET}`);
|
||||
if (result.stderr) process.stderr.write(`${RED}${result.stderr}${RESET}`);
|
||||
return;
|
||||
|
|
@ -396,7 +426,7 @@ async function cmdService(args: Args): Promise<void> {
|
|||
if (args.dumpBootstrap) {
|
||||
console.error(`Fetching bootstrap script from ${args.dumpBootstrap}...`);
|
||||
const payload = { command: "cat /tmp/bootstrap.sh" };
|
||||
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, apiKey);
|
||||
const result = await apiRequest(`/services/${args.dumpBootstrap}/execute`, "POST", payload, keys);
|
||||
|
||||
if (result.stdout) {
|
||||
const bootstrap = result.stdout;
|
||||
|
|
@ -436,7 +466,7 @@ async function cmdService(args: Args): Promise<void> {
|
|||
if (args.network) payload.network = args.network;
|
||||
if (args.vcpu) payload.vcpu = args.vcpu;
|
||||
|
||||
const result = await apiRequest("/services", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/services", "POST", payload, keys);
|
||||
console.log(`${GREEN}Service created: ${result.id || 'N/A'}${RESET}`);
|
||||
console.log(`Name: ${result.name || 'N/A'}`);
|
||||
if (result.url) console.log(`URL: ${result.url}`);
|
||||
|
|
@ -468,9 +498,9 @@ function openBrowser(url: string): void {
|
|||
});
|
||||
}
|
||||
|
||||
async function validateKey(apiKey: string, shouldExtend: boolean): Promise<void> {
|
||||
async function validateKey(keys: ApiKeys, shouldExtend: boolean): Promise<void> {
|
||||
try {
|
||||
const result = await portalRequest("/keys/validate", "POST", {}, apiKey);
|
||||
const result = await portalRequest("/keys/validate", "POST", {}, keys);
|
||||
|
||||
// Handle --extend flag first
|
||||
if (shouldExtend) {
|
||||
|
|
@ -513,8 +543,8 @@ async function validateKey(apiKey: string, shouldExtend: boolean): Promise<void>
|
|||
}
|
||||
|
||||
async function cmdKey(args: Args): Promise<void> {
|
||||
const apiKey = getApiKey(args.apiKey);
|
||||
await validateKey(apiKey, args.extend);
|
||||
const keys = getApiKeys(args.apiKey);
|
||||
await validateKey(keys, args.extend);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Args {
|
||||
|
|
|
|||
69
un.v
69
un.v
|
|
@ -126,7 +126,10 @@ fn extract_json_string(json string, key string) string {
|
|||
}
|
||||
|
||||
fn cmd_key(extend bool, api_key string) {
|
||||
cmd := "curl -s -X POST '${portal_base}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{}'"
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
body := '{}'
|
||||
cmd := "BODY='${body}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/keys/validate:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${portal_base}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
|
||||
result := exec_curl(cmd)
|
||||
|
||||
public_key := extract_json_string(result, 'public_key')
|
||||
|
|
@ -229,19 +232,24 @@ fn cmd_execute(source_file string, envs []string, artifacts bool, network string
|
|||
}
|
||||
json += '}'
|
||||
|
||||
cmd := "curl -s -X POST '${api_base}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'"
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/execute:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
|
||||
println(exec_curl(cmd))
|
||||
}
|
||||
|
||||
fn cmd_session(list bool, kill string, shell string, network string, vcpu int, tmux bool, screen bool, api_key string) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
if list {
|
||||
cmd := "curl -s -X GET '${api_base}/sessions' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/sessions:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/sessions' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if kill != '' {
|
||||
cmd := "curl -s -X DELETE '${api_base}/sessions/${kill}' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:DELETE:/sessions/${kill}:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X DELETE '${api_base}/sessions/${kill}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Session terminated: ${kill}${reset}')
|
||||
return
|
||||
|
|
@ -264,51 +272,54 @@ fn cmd_session(list bool, kill string, shell string, network string, vcpu int, t
|
|||
json += '}'
|
||||
|
||||
println('${yellow}Creating session...${reset}')
|
||||
cmd := "curl -s -X POST '${api_base}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'"
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/sessions:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
|
||||
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) {
|
||||
pub_key := get_public_key()
|
||||
secret_key := get_secret_key()
|
||||
|
||||
if list {
|
||||
cmd := "curl -s -X GET '${api_base}/services' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if info != '' {
|
||||
cmd := "curl -s -X GET '${api_base}/services/${info}' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services/${info}:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services/${info}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if logs != '' {
|
||||
cmd := "curl -s -X GET '${api_base}/services/${logs}/logs' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services/${logs}/logs:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services/${logs}/logs' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
print(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if tail != '' {
|
||||
cmd := "curl -s -X GET '${api_base}/services/${tail}/logs?lines=9000' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:GET:/services/${tail}/logs:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X GET '${api_base}/services/${tail}/logs?lines=9000' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
print(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
||||
if sleep != '' {
|
||||
cmd := "curl -s -X POST '${api_base}/services/${sleep}/sleep' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${sleep}/sleep:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${sleep}/sleep' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Service sleeping: ${sleep}${reset}')
|
||||
return
|
||||
}
|
||||
|
||||
if wake != '' {
|
||||
cmd := "curl -s -X POST '${api_base}/services/${wake}/wake' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${wake}/wake:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${wake}/wake' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Service waking: ${wake}${reset}')
|
||||
return
|
||||
}
|
||||
|
||||
if destroy != '' {
|
||||
cmd := "curl -s -X DELETE '${api_base}/services/${destroy}' -H 'Authorization: Bearer ${api_key}'"
|
||||
cmd := "TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:DELETE:/services/${destroy}:\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X DELETE '${api_base}/services/${destroy}' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\""
|
||||
exec_curl(cmd)
|
||||
println('${green}Service destroyed: ${destroy}${reset}')
|
||||
return
|
||||
|
|
@ -316,7 +327,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
|
|||
|
||||
if execute != '' {
|
||||
json := '{"command":"${escape_json(command)}"}'
|
||||
cmd := "curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'"
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${execute}/execute:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${execute}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
|
||||
result := exec_curl(cmd)
|
||||
|
||||
stdout_str := extract_json_string(result, 'stdout')
|
||||
|
|
@ -332,7 +343,8 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
|
|||
|
||||
if dump_bootstrap != '' {
|
||||
eprintln('Fetching bootstrap script from ${dump_bootstrap}...')
|
||||
cmd := "curl -s -X POST '${api_base}/services/${dump_bootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '{\"command\":\"cat /tmp/bootstrap.sh\"}'"
|
||||
json := '{"command":"cat /tmp/bootstrap.sh"}'
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services/${dump_bootstrap}/execute:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services/${dump_bootstrap}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
|
||||
result := exec_curl(cmd)
|
||||
|
||||
bootstrap_script := extract_json_string(result, 'stdout')
|
||||
|
|
@ -379,7 +391,7 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
|
|||
json += '}'
|
||||
|
||||
println('${yellow}Creating service...${reset}')
|
||||
cmd := "curl -s -X POST '${api_base}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${api_key}' -d '${json}'"
|
||||
cmd := "BODY='${json}'; TIMESTAMP=\\$(date +%s); MESSAGE=\"\\$TIMESTAMP:POST:/services:\\$BODY\"; SIGNATURE=\\$(echo -n \"\\$MESSAGE\" | openssl dgst -sha256 -hmac '${secret_key}' -hex | sed 's/.*= //'); curl -s -X POST '${api_base}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer ${pub_key}' -H \"X-Timestamp: \\$TIMESTAMP\" -H \"X-Signature: \\$SIGNATURE\" -d \"\\$BODY\""
|
||||
println(exec_curl(cmd))
|
||||
return
|
||||
}
|
||||
|
|
@ -388,8 +400,33 @@ fn cmd_service(name string, ports string, service_type string, bootstrap string,
|
|||
exit(1)
|
||||
}
|
||||
|
||||
fn get_public_key() string {
|
||||
pub_key := os.getenv('UNSANDBOX_PUBLIC_KEY')
|
||||
if pub_key != '' {
|
||||
return pub_key
|
||||
}
|
||||
api_key := os.getenv('UNSANDBOX_API_KEY')
|
||||
if api_key != '' {
|
||||
return api_key
|
||||
}
|
||||
eprintln('${red}Error: UNSANDBOX_PUBLIC_KEY or UNSANDBOX_API_KEY environment variable not set${reset}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fn get_secret_key() string {
|
||||
sec_key := os.getenv('UNSANDBOX_SECRET_KEY')
|
||||
if sec_key != '' {
|
||||
return sec_key
|
||||
}
|
||||
api_key := os.getenv('UNSANDBOX_API_KEY')
|
||||
if api_key != '' {
|
||||
return api_key
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut api_key := os.getenv('UNSANDBOX_API_KEY')
|
||||
mut api_key := get_public_key()
|
||||
|
||||
if os.args.len < 2 {
|
||||
eprintln('Usage: ${os.args[0]} [options] <source_file>')
|
||||
|
|
|
|||
144
un.zig
144
un.zig
|
|
@ -50,10 +50,57 @@ const std = @import("std");
|
|||
const fs = std.fs;
|
||||
const process = std.process;
|
||||
const mem = std.mem;
|
||||
const time = std.time;
|
||||
|
||||
const API_BASE = "https://api.unsandbox.com";
|
||||
const PORTAL_BASE = "https://unsandbox.com";
|
||||
|
||||
fn computeHmacCmd(allocator: std.mem.Allocator, secret_key: []const u8, message: []const u8) ![]const u8 {
|
||||
return try std.fmt.allocPrint(allocator, "echo -n '{s}' | openssl dgst -sha256 -hmac '{s}' -hex 2>/dev/null | sed 's/.*= //'", .{ message, secret_key });
|
||||
}
|
||||
|
||||
fn getTimestamp(allocator: std.mem.Allocator) ![]const u8 {
|
||||
const timestamp = std.time.timestamp();
|
||||
return try std.fmt.allocPrint(allocator, "{d}", .{timestamp});
|
||||
}
|
||||
|
||||
fn buildAuthCmd(allocator: std.mem.Allocator, method: []const u8, path: []const u8, body: []const u8, public_key: []const u8, secret_key: []const u8) ![]const u8 {
|
||||
if (secret_key.len == 0) {
|
||||
// Legacy mode: use public_key as bearer token
|
||||
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}'", .{public_key});
|
||||
}
|
||||
|
||||
// HMAC mode
|
||||
const timestamp_str = try getTimestamp(allocator);
|
||||
defer allocator.free(timestamp_str);
|
||||
|
||||
const message = try std.fmt.allocPrint(allocator, "{s}:{s}:{s}:{s}", .{ timestamp_str, method, path, body });
|
||||
defer allocator.free(message);
|
||||
|
||||
const hmac_cmd = try computeHmacCmd(allocator, secret_key, message);
|
||||
defer allocator.free(hmac_cmd);
|
||||
|
||||
// Execute HMAC command to get signature
|
||||
var signature_buf: [256]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&signature_buf);
|
||||
const signature_len = blk: {
|
||||
const result = try std.process.Child.run(.{
|
||||
.allocator = allocator,
|
||||
.argv = &[_][]const u8{ "sh", "-c", hmac_cmd },
|
||||
});
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const trimmed = mem.trim(u8, result.stdout, &std.ascii.whitespace);
|
||||
@memcpy(signature_buf[0..trimmed.len], trimmed);
|
||||
break :blk trimmed.len;
|
||||
};
|
||||
|
||||
const signature = signature_buf[0..signature_len];
|
||||
|
||||
return try std.fmt.allocPrint(allocator, "-H 'Authorization: Bearer {s}' -H 'X-Timestamp: {s}' -H 'X-Signature: {s}'", .{ public_key, timestamp_str, signature });
|
||||
}
|
||||
|
||||
pub fn main() !u8 {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
|
|
@ -70,10 +117,16 @@ pub fn main() !u8 {
|
|||
return 1;
|
||||
}
|
||||
|
||||
const api_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch blk: {
|
||||
var public_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_PUBLIC_KEY") catch blk: {
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
break :blk std.process.getEnvVarOwned(allocator, "UNSANDBOX_API_KEY") catch try allocator.dupe(u8, "");
|
||||
};
|
||||
defer allocator.free(public_key);
|
||||
|
||||
const secret_key = std.process.getEnvVarOwned(allocator, "UNSANDBOX_SECRET_KEY") catch blk: {
|
||||
break :blk try allocator.dupe(u8, "");
|
||||
};
|
||||
defer allocator.free(api_key);
|
||||
defer allocator.free(secret_key);
|
||||
|
||||
// Handle session command
|
||||
if (mem.eql(u8, args[1], "session")) {
|
||||
|
|
@ -90,22 +143,36 @@ pub fn main() !u8 {
|
|||
} else if (mem.eql(u8, args[i], "--shell") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
shell = args[i];
|
||||
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
allocator.free(public_key);
|
||||
public_key = try allocator.dupe(u8, args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (list) {
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/sessions' -H 'Authorization: Bearer {s}'", .{ API_BASE, api_key });
|
||||
const auth_headers = try buildAuthCmd(allocator, "GET", "/sessions", "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/sessions' {s}", .{ API_BASE, auth_headers });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
std.debug.print("\n", .{});
|
||||
} else if (kill) |k| {
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}/sessions/{s}' -H 'Authorization: Bearer {s}'", .{ API_BASE, k, api_key });
|
||||
const path = try std.fmt.allocPrint(allocator, "/sessions/{s}", .{k});
|
||||
defer allocator.free(path);
|
||||
const auth_headers = try buildAuthCmd(allocator, "DELETE", path, "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X DELETE '{s}/sessions/{s}' {s}", .{ API_BASE, k, auth_headers });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
std.debug.print("\x1b[32mSession terminated: {s}\x1b[0m\n", .{k});
|
||||
} else {
|
||||
const sh = shell orelse "bash";
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/sessions' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"shell\":\"{s}\"}}'", .{ API_BASE, api_key, sh });
|
||||
const json = try std.fmt.allocPrint(allocator, "{{\"shell\":\"{s}\"}}", .{sh});
|
||||
defer allocator.free(json);
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", "/sessions", json, public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/sessions' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json });
|
||||
defer allocator.free(cmd);
|
||||
std.debug.print("\x1b[33mCreating session...\x1b[0m\n", .{});
|
||||
_ = std.c.system(cmd.ptr);
|
||||
|
|
@ -153,29 +220,50 @@ pub fn main() !u8 {
|
|||
} else if (mem.eql(u8, args[i], "--dump-file") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
dump_file = args[i];
|
||||
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
allocator.free(public_key);
|
||||
public_key = try allocator.dupe(u8, args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (list) {
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services' -H 'Authorization: Bearer {s}'", .{ API_BASE, api_key });
|
||||
const auth_headers = try buildAuthCmd(allocator, "GET", "/services", "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services' {s}", .{ API_BASE, auth_headers });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
std.debug.print("\n", .{});
|
||||
} else if (info) |inf| {
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services/{s}' -H 'Authorization: Bearer {s}'", .{ API_BASE, inf, api_key });
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}", .{inf});
|
||||
defer allocator.free(path);
|
||||
const auth_headers = try buildAuthCmd(allocator, "GET", path, "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X GET '{s}/services/{s}' {s}", .{ API_BASE, inf, auth_headers });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
std.debug.print("\n", .{});
|
||||
} else if (execute) |exec_id| {
|
||||
const cmd_text = command orelse "";
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"{s}\"}}'", .{ API_BASE, exec_id, api_key, cmd_text });
|
||||
const json = try std.fmt.allocPrint(allocator, "{{\"command\":\"{s}\"}}", .{cmd_text});
|
||||
defer allocator.free(json);
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{exec_id});
|
||||
defer allocator.free(path);
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, exec_id, auth_headers, json });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
std.debug.print("\n", .{});
|
||||
} else if (dump_bootstrap) |bootstrap_id| {
|
||||
std.debug.print("Fetching bootstrap script from {s}...\n", .{bootstrap_id});
|
||||
const tmp_file = "/tmp/unsandbox_bootstrap_dump.txt";
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{{\"command\":\"cat /tmp/bootstrap.sh\"}}' -o {s}", .{ API_BASE, bootstrap_id, api_key, tmp_file });
|
||||
const json = "{{\"command\":\"cat /tmp/bootstrap.sh\"}}";
|
||||
const path = try std.fmt.allocPrint(allocator, "/services/{s}/execute", .{bootstrap_id});
|
||||
defer allocator.free(path);
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", path, json, public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services/{s}/execute' -H 'Content-Type: application/json' {s} -d '{s}' -o {s}", .{ API_BASE, bootstrap_id, auth_headers, json, tmp_file });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
|
||||
|
|
@ -231,7 +319,9 @@ pub fn main() !u8 {
|
|||
try writer.writeAll("}");
|
||||
const json_str = json_stream.getWritten();
|
||||
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d '{s}'", .{ API_BASE, api_key, json_str });
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", "/services", json_str, public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/services' -H 'Content-Type: application/json' {s} -d '{s}'", .{ API_BASE, auth_headers, json_str });
|
||||
defer allocator.free(cmd);
|
||||
std.debug.print("\x1b[33mCreating service...\x1b[0m\n", .{});
|
||||
_ = std.c.system(cmd.ptr);
|
||||
|
|
@ -247,13 +337,19 @@ pub fn main() !u8 {
|
|||
while (i < args.len) : (i += 1) {
|
||||
if (mem.eql(u8, args[i], "--extend")) {
|
||||
extend = true;
|
||||
} else if (mem.eql(u8, args[i], "-k") and i + 1 < args.len) {
|
||||
i += 1;
|
||||
allocator.free(public_key);
|
||||
public_key = try allocator.dupe(u8, args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (extend) {
|
||||
// First validate to get the public_key
|
||||
const json_file = "/tmp/unsandbox_key_validate.json";
|
||||
const cmd_validate = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -o {s}", .{ PORTAL_BASE, api_key, json_file });
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", "/keys/validate", "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd_validate = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' {s} -o {s}", .{ PORTAL_BASE, auth_headers, json_file });
|
||||
defer allocator.free(cmd_validate);
|
||||
_ = std.c.system(cmd_validate.ptr);
|
||||
|
||||
|
|
@ -268,15 +364,15 @@ pub fn main() !u8 {
|
|||
|
||||
// Simple JSON parsing to find public_key (looking for "public_key":"value")
|
||||
const pk_prefix = "\"public_key\":\"";
|
||||
var public_key: ?[]const u8 = null;
|
||||
var public_key_value: ?[]const u8 = null;
|
||||
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
|
||||
const value_start = start_idx + pk_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
public_key = json_content[value_start..end_idx];
|
||||
public_key_value = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
if (public_key) |pk| {
|
||||
if (public_key_value) |pk| {
|
||||
const url = try std.fmt.allocPrint(allocator, "{s}/keys/extend?pk={s}", .{ PORTAL_BASE, pk });
|
||||
defer allocator.free(url);
|
||||
std.debug.print("\x1b[33mOpening browser to extend key...\x1b[0m\n", .{});
|
||||
|
|
@ -290,7 +386,9 @@ pub fn main() !u8 {
|
|||
} else {
|
||||
// Regular validation
|
||||
const json_file = "/tmp/unsandbox_key_validate.json";
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -o {s}", .{ PORTAL_BASE, api_key, json_file });
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", "/keys/validate", "", public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/keys/validate' -H 'Content-Type: application/json' {s} -o {s}", .{ PORTAL_BASE, auth_headers, json_file });
|
||||
defer allocator.free(cmd);
|
||||
_ = std.c.system(cmd.ptr);
|
||||
|
||||
|
|
@ -319,7 +417,7 @@ pub fn main() !u8 {
|
|||
}
|
||||
|
||||
// Extract other fields
|
||||
var public_key: ?[]const u8 = null;
|
||||
var pub_key: ?[]const u8 = null;
|
||||
var tier: ?[]const u8 = null;
|
||||
var expires_at: ?[]const u8 = null;
|
||||
|
||||
|
|
@ -327,7 +425,7 @@ pub fn main() !u8 {
|
|||
if (mem.indexOf(u8, json_content, pk_prefix)) |start_idx| {
|
||||
const value_start = start_idx + pk_prefix.len;
|
||||
if (mem.indexOfPos(u8, json_content, value_start, "\"")) |end_idx| {
|
||||
public_key = json_content[value_start..end_idx];
|
||||
pub_key = json_content[value_start..end_idx];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -351,12 +449,12 @@ pub fn main() !u8 {
|
|||
if (status) |s| {
|
||||
if (mem.eql(u8, s, "valid")) {
|
||||
std.debug.print("\x1b[32mValid\x1b[0m\n", .{});
|
||||
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
|
||||
if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
|
||||
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
|
||||
if (expires_at) |exp| std.debug.print("Expires: {s}\n", .{exp});
|
||||
} else if (mem.eql(u8, s, "expired")) {
|
||||
std.debug.print("\x1b[31mExpired\x1b[0m\n", .{});
|
||||
if (public_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
|
||||
if (pub_key) |pk| std.debug.print("Public Key: {s}\n", .{pk});
|
||||
if (tier) |t| std.debug.print("Tier: {s}\n", .{t});
|
||||
if (expires_at) |exp| std.debug.print("Expired: {s}\n", .{exp});
|
||||
std.debug.print("\x1b[33mTo renew: Visit {s}/keys/extend\x1b[0m\n", .{PORTAL_BASE});
|
||||
|
|
@ -430,8 +528,14 @@ pub fn main() !u8 {
|
|||
}
|
||||
try writer.writeAll("\"}");
|
||||
|
||||
// Read back the JSON to compute HMAC
|
||||
const json_content = try fs.cwd().readFileAlloc(allocator, json_file, 10 * 1024 * 1024);
|
||||
defer allocator.free(json_content);
|
||||
|
||||
// Execute with curl
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' -H 'Authorization: Bearer {s}' -d @{s}", .{ API_BASE, api_key, json_file });
|
||||
const auth_headers = try buildAuthCmd(allocator, "POST", "/execute", json_content, public_key, secret_key);
|
||||
defer allocator.free(auth_headers);
|
||||
const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s}", .{ API_BASE, auth_headers, json_file });
|
||||
defer allocator.free(cmd);
|
||||
|
||||
const result = std.c.system(cmd.ptr);
|
||||
|
|
|
|||
92
un_deno.ts
92
un_deno.ts
|
|
@ -63,13 +63,28 @@ const EXT_MAP: Record<string, string> = {
|
|||
m: "objc",
|
||||
};
|
||||
|
||||
function getApiKey(): string {
|
||||
const key = Deno.env.get("UNSANDBOX_API_KEY");
|
||||
if (!key) {
|
||||
console.error(`${RED}Error: UNSANDBOX_API_KEY not set${RESET}`);
|
||||
Deno.exit(1);
|
||||
interface ApiKeys {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
}
|
||||
|
||||
function getApiKeys(): ApiKeys {
|
||||
let publicKey = Deno.env.get("UNSANDBOX_PUBLIC_KEY");
|
||||
let secretKey = Deno.env.get("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
const oldKey = Deno.env.get("UNSANDBOX_API_KEY");
|
||||
if (oldKey) {
|
||||
publicKey = oldKey;
|
||||
secretKey = oldKey;
|
||||
} else {
|
||||
console.error(`${RED}Error: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set${RESET}`);
|
||||
console.error(`${RED} (or legacy UNSANDBOX_API_KEY for backwards compatibility)${RESET}`);
|
||||
Deno.exit(1);
|
||||
}
|
||||
}
|
||||
return key;
|
||||
|
||||
return { publicKey, secretKey };
|
||||
}
|
||||
|
||||
function detectLanguage(filename: string): string {
|
||||
|
|
@ -92,13 +107,40 @@ async function apiRequest(
|
|||
endpoint: string,
|
||||
method: string,
|
||||
data?: unknown,
|
||||
apiKey?: string,
|
||||
keys?: ApiKeys,
|
||||
baseUrl?: string,
|
||||
): Promise<any> {
|
||||
const base = baseUrl || API_BASE;
|
||||
const url = `${base}${endpoint}`;
|
||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||
const body = data ? JSON.stringify(data) : '';
|
||||
|
||||
// Parse URL to get pathname and search
|
||||
const urlObj = new URL(url);
|
||||
const message = `${timestamp}:${method}:${urlObj.pathname}${urlObj.search}:${body}`;
|
||||
|
||||
// Create HMAC signature using Web Crypto API
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(keys!.secretKey);
|
||||
const messageData = encoder.encode(message);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
|
||||
const signature = Array.from(new Uint8Array(signatureBuffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Authorization: `Bearer ${keys!.publicKey}`,
|
||||
"X-Timestamp": timestamp,
|
||||
"X-Signature": signature,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
|
|
@ -108,7 +150,7 @@ async function apiRequest(
|
|||
};
|
||||
|
||||
if (data && method !== "GET") {
|
||||
options.body = JSON.stringify(data);
|
||||
options.body = body;
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
|
@ -124,7 +166,7 @@ async function apiRequest(
|
|||
}
|
||||
|
||||
async function cmdExecute(args: string[]) {
|
||||
const apiKey = getApiKey();
|
||||
const keys = getApiKeys();
|
||||
let sourceFile = "";
|
||||
const envVars: Record<string, string> = {};
|
||||
const inputFiles: string[] = [];
|
||||
|
|
@ -228,7 +270,7 @@ async function cmdExecute(args: string[]) {
|
|||
}
|
||||
|
||||
// Execute
|
||||
const result = await apiRequest("/execute", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/execute", "POST", payload, keys);
|
||||
|
||||
// Print output
|
||||
if (result.stdout) {
|
||||
|
|
@ -255,7 +297,7 @@ async function cmdExecute(args: string[]) {
|
|||
}
|
||||
|
||||
async function cmdSession(args: string[]) {
|
||||
const apiKey = getApiKey();
|
||||
const keys = getApiKeys();
|
||||
let listMode = false;
|
||||
let killId = "";
|
||||
let shell = "";
|
||||
|
|
@ -293,7 +335,7 @@ async function cmdSession(args: string[]) {
|
|||
}
|
||||
|
||||
if (listMode) {
|
||||
const result = await apiRequest("/sessions", "GET", undefined, apiKey);
|
||||
const result = await apiRequest("/sessions", "GET", undefined, keys);
|
||||
const sessions = result.sessions || [];
|
||||
if (sessions.length === 0) {
|
||||
console.log("No active sessions");
|
||||
|
|
@ -311,7 +353,7 @@ async function cmdSession(args: string[]) {
|
|||
}
|
||||
|
||||
if (killId) {
|
||||
await apiRequest(`/sessions/${killId}`, "DELETE", undefined, apiKey);
|
||||
await apiRequest(`/sessions/${killId}`, "DELETE", undefined, keys);
|
||||
console.log(`${GREEN}Session terminated: ${killId}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -324,7 +366,7 @@ async function cmdSession(args: string[]) {
|
|||
if (vcpu > 0) payload.vcpu = vcpu;
|
||||
|
||||
console.log(`${YELLOW}Creating session...${RESET}`);
|
||||
const result = await apiRequest("/sessions", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/sessions", "POST", payload, keys);
|
||||
console.log(`${GREEN}Session created: ${result.id}${RESET}`);
|
||||
console.log(
|
||||
`${YELLOW}(Interactive sessions require WebSocket - use un2 for full support)${RESET}`,
|
||||
|
|
@ -332,7 +374,7 @@ async function cmdSession(args: string[]) {
|
|||
}
|
||||
|
||||
async function cmdKey(args: string[]) {
|
||||
const apiKey = getApiKey();
|
||||
const keys = getApiKeys();
|
||||
let extend = false;
|
||||
|
||||
// Parse arguments
|
||||
|
|
@ -343,7 +385,7 @@ async function cmdKey(args: string[]) {
|
|||
}
|
||||
|
||||
try {
|
||||
const result = await apiRequest("/keys/validate", "POST", undefined, apiKey, PORTAL_BASE);
|
||||
const result = await apiRequest("/keys/validate", "POST", undefined, keys, PORTAL_BASE);
|
||||
|
||||
// Handle --extend flag
|
||||
if (extend) {
|
||||
|
|
@ -395,7 +437,7 @@ async function cmdKey(args: string[]) {
|
|||
}
|
||||
|
||||
async function cmdService(args: string[]) {
|
||||
const apiKey = getApiKey();
|
||||
const keys = getApiKeys();
|
||||
let listMode = false;
|
||||
let infoId = "";
|
||||
let logsId = "";
|
||||
|
|
@ -475,7 +517,7 @@ async function cmdService(args: string[]) {
|
|||
}
|
||||
|
||||
if (listMode) {
|
||||
const result = await apiRequest("/services", "GET", undefined, apiKey);
|
||||
const result = await apiRequest("/services", "GET", undefined, keys);
|
||||
const services = result.services || [];
|
||||
if (services.length === 0) {
|
||||
console.log("No services");
|
||||
|
|
@ -495,31 +537,31 @@ async function cmdService(args: string[]) {
|
|||
}
|
||||
|
||||
if (infoId) {
|
||||
const result = await apiRequest(`/services/${infoId}`, "GET", undefined, apiKey);
|
||||
const result = await apiRequest(`/services/${infoId}`, "GET", undefined, keys);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (logsId) {
|
||||
const result = await apiRequest(`/services/${logsId}/logs`, "GET", undefined, apiKey);
|
||||
const result = await apiRequest(`/services/${logsId}/logs`, "GET", undefined, keys);
|
||||
console.log(result.logs || "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sleepId) {
|
||||
await apiRequest(`/services/${sleepId}/sleep`, "POST", undefined, apiKey);
|
||||
await apiRequest(`/services/${sleepId}/sleep`, "POST", undefined, keys);
|
||||
console.log(`${GREEN}Service sleeping: ${sleepId}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wakeId) {
|
||||
await apiRequest(`/services/${wakeId}/wake`, "POST", undefined, apiKey);
|
||||
await apiRequest(`/services/${wakeId}/wake`, "POST", undefined, keys);
|
||||
console.log(`${GREEN}Service waking: ${wakeId}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (destroyId) {
|
||||
await apiRequest(`/services/${destroyId}`, "DELETE", undefined, apiKey);
|
||||
await apiRequest(`/services/${destroyId}`, "DELETE", undefined, keys);
|
||||
console.log(`${GREEN}Service destroyed: ${destroyId}${RESET}`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -553,7 +595,7 @@ async function cmdService(args: string[]) {
|
|||
if (network) payload.network = network;
|
||||
if (vcpu > 0) payload.vcpu = vcpu;
|
||||
|
||||
const result = await apiRequest("/services", "POST", payload, apiKey);
|
||||
const result = await apiRequest("/services", "POST", payload, keys);
|
||||
console.log(`${GREEN}Service created: ${result.id}${RESET}`);
|
||||
console.log(`Name: ${result.name}`);
|
||||
if (result.url) {
|
||||
|
|
|
|||
178
un_inception.c
178
un_inception.c
|
|
@ -48,6 +48,7 @@
|
|||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#define API_BASE "https://api.unsandbox.com"
|
||||
#define PORTAL_BASE "https://unsandbox.com"
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
#define GREEN "\033[32m"
|
||||
#define YELLOW "\033[33m"
|
||||
#define RESET "\033[0m"
|
||||
#define MAX_CMD_LEN 16384
|
||||
|
||||
const char* detect_language(const char *filename) {
|
||||
const char *ext = strrchr(filename, '.');
|
||||
|
|
@ -113,7 +115,54 @@ void escape_json_char(FILE *out, char c) {
|
|||
}
|
||||
}
|
||||
|
||||
void cmd_execute(const char *source_file, char **envs, int env_count, char **files, int file_count, int artifacts, const char *output_dir, const char *network, int vcpu, const char *api_key) {
|
||||
char* compute_hmac(const char *secret_key, const char *message) {
|
||||
char cmd[MAX_CMD_LEN];
|
||||
snprintf(cmd, sizeof(cmd), "echo -n '%s' | openssl dgst -sha256 -hmac '%s' -hex | sed 's/.*= //'", message, secret_key);
|
||||
|
||||
FILE *pipe = popen(cmd, "r");
|
||||
if (!pipe) return NULL;
|
||||
|
||||
static char result[128];
|
||||
if (fgets(result, sizeof(result), pipe)) {
|
||||
// Trim newline
|
||||
size_t len = strlen(result);
|
||||
while (len > 0 && (result[len-1] == '\n' || result[len-1] == '\r')) {
|
||||
result[--len] = '\0';
|
||||
}
|
||||
}
|
||||
pclose(pipe);
|
||||
return result;
|
||||
}
|
||||
|
||||
void get_timestamp(char *buffer, size_t bufsize) {
|
||||
snprintf(buffer, bufsize, "%ld", (long)time(NULL));
|
||||
}
|
||||
|
||||
void build_auth_headers(char *buffer, size_t bufsize, const char *method, const char *path, const char *body, const char *public_key, const char *secret_key) {
|
||||
if (!secret_key || secret_key[0] == '\0') {
|
||||
// Legacy mode: use public_key as bearer token
|
||||
snprintf(buffer, bufsize, "-H 'Authorization: Bearer %s'", public_key);
|
||||
return;
|
||||
}
|
||||
|
||||
// HMAC mode
|
||||
char timestamp[32];
|
||||
get_timestamp(timestamp, sizeof(timestamp));
|
||||
|
||||
char message[MAX_CMD_LEN];
|
||||
snprintf(message, sizeof(message), "%s:%s:%s:%s", timestamp, method, path, body ? body : "");
|
||||
|
||||
char *signature = compute_hmac(secret_key, message);
|
||||
if (!signature) {
|
||||
fprintf(stderr, "%sError computing HMAC signature%s\n", RED, RESET);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
snprintf(buffer, bufsize, "-H 'Authorization: Bearer %s' -H 'X-Timestamp: %s' -H 'X-Signature: %s'",
|
||||
public_key, timestamp, signature);
|
||||
}
|
||||
|
||||
void cmd_execute(const char *source_file, char **envs, int env_count, char **files, int file_count, int artifacts, const char *output_dir, const char *network, int vcpu, const char *public_key, const char *secret_key) {
|
||||
const char *language = detect_language(source_file);
|
||||
if (!language) {
|
||||
fprintf(stderr, "%sError: Cannot detect language%s\n", RED, RESET);
|
||||
|
|
@ -179,13 +228,16 @@ void cmd_execute(const char *source_file, char **envs, int env_count, char **fil
|
|||
fclose(jsonf);
|
||||
|
||||
// Make API request using curl
|
||||
char cmd[8192];
|
||||
char auth_headers[1024];
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/execute", json_body, public_key, secret_key);
|
||||
|
||||
char cmd[MAX_CMD_LEN];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X POST '%s/execute' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer %s' "
|
||||
"%s "
|
||||
"-d @- << 'EOF'\n%s\nEOF",
|
||||
API_BASE, api_key, json_body);
|
||||
API_BASE, auth_headers, json_body);
|
||||
|
||||
FILE *curl = popen(cmd, "r");
|
||||
if (!curl) {
|
||||
|
|
@ -262,22 +314,27 @@ void cmd_execute(const char *source_file, char **envs, int env_count, char **fil
|
|||
exit(exit_code);
|
||||
}
|
||||
|
||||
void cmd_session(int list, const char *kill, const char *shell, const char *network, int vcpu, int tmux, int screen, const char *api_key) {
|
||||
char cmd[4096];
|
||||
void cmd_session(int list, const char *kill, const char *shell, const char *network, int vcpu, int tmux, int screen, const char *public_key, const char *secret_key) {
|
||||
char cmd[8192];
|
||||
char auth_headers[1024];
|
||||
|
||||
if (list) {
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", "/sessions", "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X GET '%s/sessions' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, api_key);
|
||||
"curl -s -X GET '%s/sessions' %s",
|
||||
API_BASE, auth_headers);
|
||||
system(cmd);
|
||||
printf("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (kill) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/sessions/%s", kill);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "DELETE", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X DELETE '%s/sessions/%s' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, kill, api_key);
|
||||
"curl -s -X DELETE '%s/sessions/%s' %s",
|
||||
API_BASE, kill, auth_headers);
|
||||
system(cmd);
|
||||
printf("%sSession terminated: %s%s\n", GREEN, kill, RESET);
|
||||
return;
|
||||
|
|
@ -305,22 +362,25 @@ void cmd_session(int list, const char *kill, const char *shell, const char *netw
|
|||
strcat(json, "}");
|
||||
|
||||
printf("%sCreating session...%s\n", YELLOW, RESET);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/sessions", json, public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X POST '%s/sessions' -H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer %s' -d '%s'",
|
||||
API_BASE, api_key, json);
|
||||
"%s -d '%s'",
|
||||
API_BASE, auth_headers, json);
|
||||
system(cmd);
|
||||
printf("\n%sSession created%s\n", GREEN, RESET);
|
||||
}
|
||||
|
||||
void cmd_key(int extend, const char *api_key) {
|
||||
char cmd[4096];
|
||||
void cmd_key(int extend, const char *public_key, const char *secret_key) {
|
||||
char cmd[8192];
|
||||
char auth_headers[1024];
|
||||
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/keys/validate", "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X POST '%s/keys/validate' "
|
||||
"-H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer %s'",
|
||||
PORTAL_BASE, api_key);
|
||||
"%s",
|
||||
PORTAL_BASE, auth_headers);
|
||||
|
||||
FILE *curl = popen(cmd, "r");
|
||||
if (!curl) {
|
||||
|
|
@ -449,65 +509,85 @@ void cmd_key(int extend, const char *api_key) {
|
|||
}
|
||||
}
|
||||
|
||||
void cmd_service(const char *name, const char *ports, const char *domains, const char *service_type, const char *bootstrap, int list, const char *info, const char *logs, const char *tail, const char *sleep_svc, const char *wake, const char *destroy, const char *network, int vcpu, const char *api_key) {
|
||||
char cmd[8192];
|
||||
void cmd_service(const char *name, const char *ports, const char *domains, const char *service_type, const char *bootstrap, int list, const char *info, const char *logs, const char *tail, const char *sleep_svc, const char *wake, const char *destroy, const char *network, int vcpu, const char *public_key, const char *secret_key) {
|
||||
char cmd[16384];
|
||||
char auth_headers[1024];
|
||||
|
||||
if (list) {
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", "/services", "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X GET '%s/services' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, api_key);
|
||||
"curl -s -X GET '%s/services' %s",
|
||||
API_BASE, auth_headers);
|
||||
system(cmd);
|
||||
printf("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (info) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/services/%s", info);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X GET '%s/services/%s' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, info, api_key);
|
||||
"curl -s -X GET '%s/services/%s' %s",
|
||||
API_BASE, info, auth_headers);
|
||||
system(cmd);
|
||||
printf("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (logs) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/services/%s/logs", logs);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X GET '%s/services/%s/logs' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, logs, api_key);
|
||||
"curl -s -X GET '%s/services/%s/logs' %s",
|
||||
API_BASE, logs, auth_headers);
|
||||
system(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tail) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/services/%s/logs", tail);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "GET", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X GET '%s/services/%s/logs?lines=9000' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, tail, api_key);
|
||||
"curl -s -X GET '%s/services/%s/logs?lines=9000' %s",
|
||||
API_BASE, tail, auth_headers);
|
||||
system(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sleep_svc) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/services/%s/sleep", sleep_svc);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X POST '%s/services/%s/sleep' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, sleep_svc, api_key);
|
||||
"curl -s -X POST '%s/services/%s/sleep' %s",
|
||||
API_BASE, sleep_svc, auth_headers);
|
||||
system(cmd);
|
||||
printf("%sService sleeping: %s%s\n", GREEN, sleep_svc, RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wake) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/services/%s/wake", wake);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X POST '%s/services/%s/wake' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, wake, api_key);
|
||||
"curl -s -X POST '%s/services/%s/wake' %s",
|
||||
API_BASE, wake, auth_headers);
|
||||
system(cmd);
|
||||
printf("%sService waking: %s%s\n", GREEN, wake, RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (destroy) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "/services/%s", destroy);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "DELETE", path, "", public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X DELETE '%s/services/%s' -H 'Authorization: Bearer %s'",
|
||||
API_BASE, destroy, api_key);
|
||||
"curl -s -X DELETE '%s/services/%s' %s",
|
||||
API_BASE, destroy, auth_headers);
|
||||
system(cmd);
|
||||
printf("%sService destroyed: %s%s\n", GREEN, destroy, RESET);
|
||||
return;
|
||||
|
|
@ -551,10 +631,11 @@ void cmd_service(const char *name, const char *ports, const char *domains, const
|
|||
strcat(json, "}");
|
||||
|
||||
printf("%sCreating service...%s\n", YELLOW, RESET);
|
||||
build_auth_headers(auth_headers, sizeof(auth_headers), "POST", "/services", json, public_key, secret_key);
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"curl -s -X POST '%s/services' -H 'Content-Type: application/json' "
|
||||
"-H 'Authorization: Bearer %s' -d '%s'",
|
||||
API_BASE, api_key, json);
|
||||
"%s -d '%s'",
|
||||
API_BASE, auth_headers, json);
|
||||
system(cmd);
|
||||
printf("\n%sService created%s\n", GREEN, RESET);
|
||||
return;
|
||||
|
|
@ -565,10 +646,15 @@ void cmd_service(const char *name, const char *ports, const char *domains, const
|
|||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *api_key = getenv("UNSANDBOX_API_KEY");
|
||||
if (!api_key && argc > 1 && strcmp(argv[1], "session") != 0 && strcmp(argv[1], "service") != 0) {
|
||||
fprintf(stderr, "%sError: UNSANDBOX_API_KEY not set%s\n", RED, RESET);
|
||||
return 1;
|
||||
const char *public_key = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||
const char *secret_key = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
// Fall back to UNSANDBOX_API_KEY for backwards compatibility
|
||||
if (!public_key) {
|
||||
public_key = getenv("UNSANDBOX_API_KEY");
|
||||
}
|
||||
if (!secret_key) {
|
||||
secret_key = "";
|
||||
}
|
||||
|
||||
if (argc < 2) {
|
||||
|
|
@ -584,9 +670,9 @@ int main(int argc, char *argv[]) {
|
|||
int extend = 0;
|
||||
for (int i = 2; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--extend") == 0) extend = 1;
|
||||
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i];
|
||||
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) public_key = argv[++i];
|
||||
}
|
||||
cmd_key(extend, api_key);
|
||||
cmd_key(extend, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -607,10 +693,10 @@ int main(int argc, char *argv[]) {
|
|||
else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]);
|
||||
else if (strcmp(argv[i], "--tmux") == 0) tmux = 1;
|
||||
else if (strcmp(argv[i], "--screen") == 0) screen = 1;
|
||||
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i];
|
||||
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) public_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, api_key);
|
||||
cmd_session(list, kill, shell, network, vcpu, tmux, screen, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -645,10 +731,10 @@ int main(int argc, char *argv[]) {
|
|||
else if (strcmp(argv[i], "--destroy") == 0 && i + 1 < argc) destroy = argv[++i];
|
||||
else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) network = argv[++i];
|
||||
else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) vcpu = atoi(argv[++i]);
|
||||
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) api_key = argv[++i];
|
||||
else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) public_key = argv[++i];
|
||||
}
|
||||
|
||||
cmd_service(name, ports, domains, service_type, bootstrap, list, info, logs, tail, sleep_svc, wake, destroy, network, vcpu, api_key);
|
||||
cmd_service(name, ports, domains, service_type, bootstrap, list, info, logs, tail, sleep_svc, wake, destroy, network, vcpu, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -677,7 +763,7 @@ int main(int argc, char *argv[]) {
|
|||
} else if (strcmp(argv[i], "-v") == 0 && i + 1 < argc) {
|
||||
vcpu = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "-k") == 0 && i + 1 < argc) {
|
||||
api_key = argv[++i];
|
||||
public_key = argv[++i];
|
||||
} else if (!source_file && argv[i][0] != '-') {
|
||||
source_file = argv[i];
|
||||
}
|
||||
|
|
@ -688,6 +774,6 @@ int main(int argc, char *argv[]) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
cmd_execute(source_file, envs, env_count, files, file_count, artifacts, output_dir, network, vcpu, api_key);
|
||||
cmd_execute(source_file, envs, env_count, files, file_count, artifacts, output_dir, network, vcpu, public_key, secret_key);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue