All SDKs now implement 58+ functions matching C reference (un.h): - Execution (8): execute, execute_async, wait_job, get_job, cancel_job, list_jobs, get_languages, detect_language - Sessions (9): list, get, create, destroy, freeze, unfreeze, boost, unboost, execute - Services (17): list, get, create, destroy, freeze, unfreeze, lock, unlock, set_unfreeze_on_demand, redeploy, logs, execute, env_get/set/delete/export, resize - Snapshots (9): list, get, session, service, restore, delete, lock, unlock, clone - Images (13): list, get, publish, delete, lock, unlock, set_visibility, grant/revoke_access, list_trusted, transfer, spawn, clone - PaaS Logs (2): fetch, stream - Utilities (5): validate_keys, hmac_sign, health_check, version, last_error Test suites created for all SDKs with unit, integration, and functional tests. Languages: AWK, Bash, C++, C#, Clojure, COBOL, Crystal, D, Dart, .NET, Elixir, Erlang, F#, Forth, Fortran, Go, Groovy, Haskell, Java, JavaScript, Julia, Kotlin, Lisp, Lua, Nim, Objective-C, OCaml, Perl, PHP, PowerShell, Prolog, Python, R, Raku, Ruby, Rust, Scheme, Swift, Tcl, TypeScript, V, Zig
2420 lines
99 KiB
Objective-C
2420 lines
99 KiB
Objective-C
// PUBLIC DOMAIN - NO LICENSE, NO WARRANTY
|
|
//
|
|
// This is free public domain software for the public good of a permacomputer hosted
|
|
// at permacomputer.com - an always-on computer by the people, for the people. One
|
|
// which is durable, easy to repair, and distributed like tap water for machine
|
|
// learning intelligence.
|
|
//
|
|
// The permacomputer is community-owned infrastructure optimized around four values:
|
|
//
|
|
// TRUTH - First principles, math & science, open source code freely distributed
|
|
// FREEDOM - Voluntary partnerships, freedom from tyranny & corporate control
|
|
// HARMONY - Minimal waste, self-renewing systems with diverse thriving connections
|
|
// LOVE - Be yourself without hurting others, cooperation through natural law
|
|
//
|
|
// This software contributes to that vision by enabling code execution across 42+
|
|
// programming languages through a unified interface, accessible to all. Code is
|
|
// seeds to sprout on any abandoned technology.
|
|
//
|
|
// Learn more: https://www.permacomputer.com
|
|
//
|
|
// Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
|
// software, either in source code form or as a compiled binary, for any purpose,
|
|
// commercial or non-commercial, and by any means.
|
|
//
|
|
// NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
|
|
//
|
|
// That said, our permacomputer's digital membrane stratum continuously runs unit,
|
|
// integration, and functional tests on all of it's own software - with our
|
|
// permacomputer monitoring itself, repairing itself, with minimal human in the
|
|
// loop guidance. Our agents do their best.
|
|
//
|
|
// Copyright 2025 TimeHexOn & foxhop & russell@unturf
|
|
// https://www.timehexon.com
|
|
// https://www.foxhop.net
|
|
// https://www.unturf.com/software
|
|
//
|
|
// unsandbox SDK for Objective-C - Execute code in secure sandboxes
|
|
// https://unsandbox.com | https://api.unsandbox.com/openapi
|
|
//
|
|
// Library Usage:
|
|
// #import "un.m" // or as header
|
|
// UNClient *client = [[UNClient alloc] init];
|
|
// NSDictionary *result = [client execute:@"python" code:@"print('Hello')"];
|
|
// NSLog(@"%@", result[@"stdout"]);
|
|
//
|
|
// CLI Usage:
|
|
// ./un.m script.py
|
|
// ./un.m -s python 'print("Hello")'
|
|
// ./un.m session --shell python3
|
|
//
|
|
// Authentication (in priority order):
|
|
// 1. UNClient initWithPublicKey:secretKey: constructor arguments
|
|
// 2. Environment variables: UNSANDBOX_PUBLIC_KEY + UNSANDBOX_SECRET_KEY
|
|
// 3. Config file: ~/.unsandbox/accounts.csv (public_key,secret_key per line)
|
|
|
|
#!/usr/bin/env -S clang -x objective-c -framework Foundation -o /tmp/un_objc && /tmp/un_objc
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import <CommonCrypto/CommonHMAC.h>
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
static NSString* const UN_API_BASE = @"https://api.unsandbox.com";
|
|
static NSString* const UN_PORTAL_BASE = @"https://unsandbox.com";
|
|
static const NSInteger UN_DEFAULT_TIMEOUT = 300;
|
|
static const NSInteger UN_DEFAULT_TTL = 60;
|
|
static const NSInteger UN_LANGUAGES_CACHE_TTL = 3600; // 1 hour in seconds
|
|
|
|
// Polling delays (ms) - exponential backoff
|
|
static const int UN_POLL_DELAYS[] = {300, 450, 700, 900, 650, 1600, 2000};
|
|
static const int UN_POLL_DELAYS_COUNT = 7;
|
|
|
|
// ANSI colors
|
|
static NSString* const BLUE = @"\033[34m";
|
|
static NSString* const RED = @"\033[31m";
|
|
static NSString* const GREEN = @"\033[32m";
|
|
static NSString* const YELLOW = @"\033[33m";
|
|
static NSString* const RESET = @"\033[0m";
|
|
|
|
// ============================================================================
|
|
// Extension to Language Mapping
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Returns mapping from file extensions to language identifiers.
|
|
*/
|
|
NSDictionary* UNGetExtMap(void) {
|
|
return @{
|
|
@"py": @"python", @"js": @"javascript", @"ts": @"typescript",
|
|
@"rb": @"ruby", @"php": @"php", @"pl": @"perl", @"lua": @"lua",
|
|
@"sh": @"bash", @"go": @"go", @"rs": @"rust", @"c": @"c",
|
|
@"cpp": @"cpp", @"cc": @"cpp", @"cxx": @"cpp",
|
|
@"java": @"java", @"kt": @"kotlin", @"cs": @"csharp", @"fs": @"fsharp",
|
|
@"hs": @"haskell", @"ml": @"ocaml", @"clj": @"clojure", @"scm": @"scheme",
|
|
@"lisp": @"commonlisp", @"erl": @"erlang", @"ex": @"elixir", @"exs": @"elixir",
|
|
@"jl": @"julia", @"r": @"r", @"R": @"r", @"cr": @"crystal",
|
|
@"d": @"d", @"nim": @"nim", @"zig": @"zig", @"v": @"vlang",
|
|
@"dart": @"dart", @"groovy": @"groovy", @"scala": @"scala",
|
|
@"f90": @"fortran", @"f95": @"fortran", @"cob": @"cobol",
|
|
@"pro": @"prolog", @"forth": @"forth", @"4th": @"forth",
|
|
@"tcl": @"tcl", @"raku": @"raku", @"pl6": @"raku", @"p6": @"raku",
|
|
@"m": @"objc", @"awk": @"awk"
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// Error Classes
|
|
// ============================================================================
|
|
|
|
/**
|
|
* UNError - Base error class for unsandbox SDK errors.
|
|
*/
|
|
@interface UNError : NSError
|
|
+ (instancetype)errorWithMessage:(NSString*)message;
|
|
@end
|
|
|
|
@implementation UNError
|
|
+ (instancetype)errorWithMessage:(NSString*)message {
|
|
return [self errorWithDomain:@"com.unsandbox" code:1 userInfo:@{NSLocalizedDescriptionKey: message}];
|
|
}
|
|
@end
|
|
|
|
/**
|
|
* UNAuthenticationError - Invalid or missing credentials.
|
|
*/
|
|
@interface UNAuthenticationError : UNError
|
|
@end
|
|
|
|
@implementation UNAuthenticationError
|
|
@end
|
|
|
|
/**
|
|
* UNExecutionError - Code execution failed.
|
|
*/
|
|
@interface UNExecutionError : UNError
|
|
@property (nonatomic) int exitCode;
|
|
@property (nonatomic, strong) NSString* stderr;
|
|
@end
|
|
|
|
@implementation UNExecutionError
|
|
@end
|
|
|
|
/**
|
|
* UNAPIError - API request failed.
|
|
*/
|
|
@interface UNAPIError : UNError
|
|
@property (nonatomic) NSInteger statusCode;
|
|
@property (nonatomic, strong) NSString* response;
|
|
@end
|
|
|
|
@implementation UNAPIError
|
|
@end
|
|
|
|
/**
|
|
* UNTimeoutError - Execution or polling timed out.
|
|
*/
|
|
@interface UNTimeoutError : UNError
|
|
@end
|
|
|
|
@implementation UNTimeoutError
|
|
@end
|
|
|
|
// ============================================================================
|
|
// HMAC Authentication
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Generate HMAC-SHA256 signature in hex format.
|
|
*
|
|
* @param key The secret key for HMAC
|
|
* @param message The message to sign
|
|
* @return Hex-encoded signature string
|
|
*/
|
|
NSString* UNHmacSha256Hex(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;
|
|
}
|
|
|
|
/**
|
|
* Compute API request signature.
|
|
* Signature = HMAC-SHA256(secret_key, "timestamp:METHOD:path:body")
|
|
*
|
|
* @param secretKey API secret key
|
|
* @param timestamp Unix timestamp
|
|
* @param method HTTP method (GET, POST, etc.)
|
|
* @param path API endpoint path
|
|
* @param body Request body (empty string if none)
|
|
* @return Hex-encoded signature
|
|
*/
|
|
NSString* UNComputeSignature(NSString* secretKey, long timestamp, NSString* method, NSString* path, NSString* body) {
|
|
NSString* message = [NSString stringWithFormat:@"%ld:%@:%@:%@", timestamp, method, path, body ?: @""];
|
|
return UNHmacSha256Hex(secretKey, message);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Credentials Loading
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get API credentials from environment or config file.
|
|
* Priority: 1. Arguments, 2. Environment vars, 3. ~/.unsandbox/accounts.csv
|
|
*
|
|
* @param publicKey Output public key
|
|
* @param secretKey Output secret key
|
|
* @param argPublicKey Optional public key from arguments
|
|
* @param argSecretKey Optional secret key from arguments
|
|
* @param error Error output
|
|
* @return YES if credentials found, NO otherwise
|
|
*/
|
|
BOOL UNGetCredentials(NSString** publicKey, NSString** secretKey, NSString* argPublicKey, NSString* argSecretKey, NSError** error) {
|
|
// Priority 1: Function arguments
|
|
if (argPublicKey && argSecretKey && [argPublicKey length] > 0 && [argSecretKey length] > 0) {
|
|
*publicKey = argPublicKey;
|
|
*secretKey = argSecretKey;
|
|
return YES;
|
|
}
|
|
|
|
// Priority 2: Environment variables
|
|
*publicKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_PUBLIC_KEY"];
|
|
*secretKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_SECRET_KEY"];
|
|
|
|
if (*publicKey && *secretKey && [*publicKey length] > 0 && [*secretKey length] > 0) {
|
|
return YES;
|
|
}
|
|
|
|
// Fall back to legacy UNSANDBOX_API_KEY
|
|
NSString* oldKey = [[[NSProcessInfo processInfo] environment] objectForKey:@"UNSANDBOX_API_KEY"];
|
|
if (oldKey && [oldKey length] > 0) {
|
|
*publicKey = oldKey;
|
|
*secretKey = oldKey;
|
|
return YES;
|
|
}
|
|
|
|
// Priority 3: Config file ~/.unsandbox/accounts.csv
|
|
NSString* home = NSHomeDirectory();
|
|
NSString* accountsPath = [home stringByAppendingPathComponent:@".unsandbox/accounts.csv"];
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
|
|
if ([fm fileExistsAtPath:accountsPath]) {
|
|
NSString* content = [NSString stringWithContentsOfFile:accountsPath encoding:NSUTF8StringEncoding error:nil];
|
|
if (content) {
|
|
NSArray* lines = [content componentsSeparatedByString:@"\n"];
|
|
for (NSString* line in lines) {
|
|
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
|
|
|
|
NSArray* parts = [trimmed componentsSeparatedByString:@","];
|
|
if ([parts count] >= 2) {
|
|
NSString* pk = [parts[0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
NSString* sk = [parts[1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
if ([pk hasPrefix:@"unsb-pk-"] && [sk hasPrefix:@"unsb-sk-"]) {
|
|
*publicKey = pk;
|
|
*secretKey = sk;
|
|
return YES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (error) {
|
|
*error = [UNAuthenticationError errorWithMessage:
|
|
@"No credentials found. Set UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY, "
|
|
"or create ~/.unsandbox/accounts.csv, or pass credentials to initializer."];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
/**
|
|
* Get API keys for CLI commands (exits on failure).
|
|
*/
|
|
void UNGetApiKeysCLI(NSString** publicKey, NSString** secretKey) {
|
|
NSError* error = nil;
|
|
if (!UNGetCredentials(publicKey, secretKey, nil, nil, &error)) {
|
|
fprintf(stderr, "%s%s%s\n", [RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Clock Drift Detection
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check response for timestamp/clock drift errors.
|
|
*/
|
|
void UNCheckClockDrift(NSString* response) {
|
|
NSString* responseLower = [response lowercaseString];
|
|
if ([responseLower rangeOfString:@"timestamp"].location != NSNotFound &&
|
|
([responseLower rangeOfString:@"401"].location != NSNotFound ||
|
|
[responseLower rangeOfString:@"expired"].location != NSNotFound ||
|
|
[responseLower rangeOfString:@"invalid"].location != NSNotFound)) {
|
|
fprintf(stderr, "%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n",
|
|
[RED UTF8String], [RESET UTF8String]);
|
|
fprintf(stderr, "%sYour computer's clock may have drifted.%s\n",
|
|
[YELLOW UTF8String], [RESET UTF8String]);
|
|
fprintf(stderr, "Check your system time and sync with NTP if needed:\n");
|
|
fprintf(stderr, " Linux: sudo ntpdate -s time.nist.gov\n");
|
|
fprintf(stderr, " macOS: sudo sntp -sS time.apple.com\n");
|
|
fprintf(stderr, " Windows: w32tm /resync\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Languages Cache
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get path to languages cache file.
|
|
*/
|
|
NSString* UNLanguagesCachePath(void) {
|
|
NSString* home = NSHomeDirectory();
|
|
return [home stringByAppendingPathComponent:@".unsandbox/languages.json"];
|
|
}
|
|
|
|
/**
|
|
* Check if languages cache is valid (less than 1 hour old).
|
|
*/
|
|
BOOL UNIsCacheValid(void) {
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
NSString* cachePath = UNLanguagesCachePath();
|
|
|
|
if (![fm fileExistsAtPath:cachePath]) {
|
|
return NO;
|
|
}
|
|
|
|
NSError* error = nil;
|
|
NSDictionary* attrs = [fm attributesOfItemAtPath:cachePath error:&error];
|
|
if (error) {
|
|
return NO;
|
|
}
|
|
|
|
NSDate* modDate = attrs[NSFileModificationDate];
|
|
NSTimeInterval age = -[modDate timeIntervalSinceNow];
|
|
return age < UN_LANGUAGES_CACHE_TTL;
|
|
}
|
|
|
|
/**
|
|
* Read languages from cache file.
|
|
*/
|
|
NSDictionary* UNReadLanguagesCache(void) {
|
|
NSString* cachePath = UNLanguagesCachePath();
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
|
|
if (![fm fileExistsAtPath:cachePath]) {
|
|
return nil;
|
|
}
|
|
|
|
NSData* data = [NSData dataWithContentsOfFile:cachePath];
|
|
if (!data) {
|
|
return nil;
|
|
}
|
|
|
|
NSError* error = nil;
|
|
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
|
return error ? nil : result;
|
|
}
|
|
|
|
/**
|
|
* Write languages to cache file.
|
|
*/
|
|
void UNWriteLanguagesCache(NSDictionary* data) {
|
|
NSString* cachePath = UNLanguagesCachePath();
|
|
NSString* cacheDir = [cachePath stringByDeletingLastPathComponent];
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
|
|
// Create directory if needed
|
|
if (![fm fileExistsAtPath:cacheDir]) {
|
|
[fm createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:nil];
|
|
}
|
|
|
|
NSError* error = nil;
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
|
|
if (!error && jsonData) {
|
|
[jsonData writeToFile:cachePath atomically:YES];
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Language Detection
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Detect programming language from file extension or shebang.
|
|
*
|
|
* @param filename Path to source file
|
|
* @return Language identifier or nil if undetected
|
|
*/
|
|
NSString* UNDetectLanguage(NSString* filename) {
|
|
NSString* ext = [filename pathExtension];
|
|
NSDictionary* langMap = UNGetExtMap();
|
|
|
|
NSString* language = langMap[ext];
|
|
if (language) {
|
|
return language;
|
|
}
|
|
|
|
// Try reading shebang
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
if ([fm fileExistsAtPath:filename]) {
|
|
NSString* content = [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil];
|
|
if (content) {
|
|
NSString* firstLine = [[content componentsSeparatedByString:@"\n"] firstObject];
|
|
if ([firstLine hasPrefix:@"#!"]) {
|
|
if ([firstLine rangeOfString:@"python"].location != NSNotFound) return @"python";
|
|
if ([firstLine rangeOfString:@"node"].location != NSNotFound) return @"javascript";
|
|
if ([firstLine rangeOfString:@"ruby"].location != NSNotFound) return @"ruby";
|
|
if ([firstLine rangeOfString:@"perl"].location != NSNotFound) return @"perl";
|
|
if ([firstLine rangeOfString:@"bash"].location != NSNotFound ||
|
|
[firstLine rangeOfString:@"/sh"].location != NSNotFound) return @"bash";
|
|
if ([firstLine rangeOfString:@"lua"].location != NSNotFound) return @"lua";
|
|
if ([firstLine rangeOfString:@"php"].location != NSNotFound) return @"php";
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
// ============================================================================
|
|
// UNClient Class - Main SDK Interface
|
|
// ============================================================================
|
|
|
|
/**
|
|
* UNClient - Unsandbox API client with stored credentials.
|
|
*
|
|
* Example usage:
|
|
* UNClient *client = [[UNClient alloc] init];
|
|
* NSDictionary *result = [client execute:@"python" code:@"print('Hello')"];
|
|
* NSLog(@"Output: %@", result[@"stdout"]);
|
|
*
|
|
* // Or with explicit credentials:
|
|
* UNClient *client = [[UNClient alloc] initWithPublicKey:@"unsb-pk-..." secretKey:@"unsb-sk-..."];
|
|
*/
|
|
@interface UNClient : NSObject
|
|
|
|
@property (nonatomic, strong, readonly) NSString* publicKey;
|
|
@property (nonatomic, strong, readonly) NSString* secretKey;
|
|
|
|
/**
|
|
* Initialize client with automatic credential loading.
|
|
* Loads from environment variables or ~/.unsandbox/accounts.csv
|
|
*/
|
|
- (instancetype)init;
|
|
|
|
/**
|
|
* Initialize client with explicit credentials.
|
|
*
|
|
* @param publicKey API public key (unsb-pk-...)
|
|
* @param secretKey API secret key (unsb-sk-...)
|
|
*/
|
|
- (instancetype)initWithPublicKey:(NSString*)publicKey secretKey:(NSString*)secretKey;
|
|
|
|
/**
|
|
* Execute code synchronously.
|
|
*
|
|
* @param language Programming language (python, javascript, go, rust, etc.)
|
|
* @param code Source code to execute
|
|
* @return Dictionary with stdout, stderr, exit_code, job_id
|
|
*/
|
|
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code;
|
|
|
|
/**
|
|
* Execute code with options.
|
|
*
|
|
* @param language Programming language
|
|
* @param code Source code
|
|
* @param options Dictionary with optional keys: env, input_files, network_mode, ttl, vcpu, return_artifact
|
|
* @return Dictionary with stdout, stderr, exit_code, job_id
|
|
*/
|
|
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code options:(NSDictionary*)options;
|
|
|
|
/**
|
|
* Execute code asynchronously. Returns immediately with job_id.
|
|
*
|
|
* @param language Programming language
|
|
* @param code Source code
|
|
* @param options Optional execution options
|
|
* @return Dictionary with job_id, status ("pending")
|
|
*/
|
|
- (NSDictionary*)executeAsync:(NSString*)language code:(NSString*)code options:(NSDictionary*)options;
|
|
|
|
/**
|
|
* Execute code with automatic language detection from shebang.
|
|
*
|
|
* @param code Source code with shebang (e.g., #!/usr/bin/env python3)
|
|
* @return Dictionary with detected_language, stdout, stderr, etc.
|
|
*/
|
|
- (NSDictionary*)run:(NSString*)code;
|
|
|
|
/**
|
|
* Execute with auto-detect, asynchronously.
|
|
*
|
|
* @param code Source code with shebang
|
|
* @return Dictionary with job_id, detected_language, status
|
|
*/
|
|
- (NSDictionary*)runAsync:(NSString*)code;
|
|
|
|
/**
|
|
* Get job status and results.
|
|
*
|
|
* @param jobId Job ID from executeAsync or runAsync
|
|
* @return Dictionary with job_id, status, result (if completed)
|
|
*/
|
|
- (NSDictionary*)getJob:(NSString*)jobId;
|
|
|
|
/**
|
|
* Wait for job completion with exponential backoff polling.
|
|
*
|
|
* @param jobId Job ID to wait for
|
|
* @return Final job result dictionary
|
|
*/
|
|
- (NSDictionary*)wait:(NSString*)jobId;
|
|
|
|
/**
|
|
* Wait for job with max polls limit.
|
|
*
|
|
* @param jobId Job ID to wait for
|
|
* @param maxPolls Maximum number of poll attempts
|
|
* @return Final job result dictionary
|
|
*/
|
|
- (NSDictionary*)wait:(NSString*)jobId maxPolls:(int)maxPolls;
|
|
|
|
/**
|
|
* Cancel a running job.
|
|
*
|
|
* @param jobId Job ID to cancel
|
|
* @return Dictionary with partial output collected before cancellation
|
|
*/
|
|
- (NSDictionary*)cancelJob:(NSString*)jobId;
|
|
|
|
/**
|
|
* List all active jobs for this API key.
|
|
*
|
|
* @return Array of job summary dictionaries
|
|
*/
|
|
- (NSArray*)listJobs;
|
|
|
|
/**
|
|
* Generate images from text prompt.
|
|
*
|
|
* @param prompt Text description of the image to generate
|
|
* @return Dictionary with images array, created_at
|
|
*/
|
|
- (NSDictionary*)image:(NSString*)prompt;
|
|
|
|
/**
|
|
* Generate images with options.
|
|
*
|
|
* @param prompt Text prompt
|
|
* @param options Dictionary with optional keys: model, size, quality, n
|
|
* @return Dictionary with images array
|
|
*/
|
|
- (NSDictionary*)image:(NSString*)prompt options:(NSDictionary*)options;
|
|
|
|
/**
|
|
* Get list of supported programming languages.
|
|
* Results are cached in ~/.unsandbox/languages.json for 1 hour.
|
|
*
|
|
* @return Dictionary with languages array, count, aliases
|
|
*/
|
|
- (NSDictionary*)languages;
|
|
|
|
/**
|
|
* Make authenticated API request.
|
|
*
|
|
* @param endpoint API endpoint (e.g., /execute)
|
|
* @param method HTTP method
|
|
* @param data Request body dictionary (or nil)
|
|
* @return Response dictionary
|
|
*/
|
|
- (NSDictionary*)apiRequest:(NSString*)endpoint method:(NSString*)method data:(NSDictionary*)data;
|
|
|
|
/**
|
|
* Make API request with text/plain body.
|
|
*/
|
|
- (NSDictionary*)apiRequestText:(NSString*)endpoint method:(NSString*)method body:(NSString*)body;
|
|
|
|
@end
|
|
|
|
@implementation UNClient
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
NSString* pk = nil;
|
|
NSString* sk = nil;
|
|
NSError* error = nil;
|
|
if (!UNGetCredentials(&pk, &sk, nil, nil, &error)) {
|
|
@throw [NSException exceptionWithName:@"UNAuthenticationError"
|
|
reason:[error localizedDescription]
|
|
userInfo:nil];
|
|
}
|
|
_publicKey = pk;
|
|
_secretKey = sk;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithPublicKey:(NSString*)publicKey secretKey:(NSString*)secretKey {
|
|
self = [super init];
|
|
if (self) {
|
|
_publicKey = publicKey;
|
|
_secretKey = secretKey;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSDictionary*)apiRequest:(NSString*)endpoint method:(NSString*)method data:(NSDictionary*)data {
|
|
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlString];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
[request setHTTPMethod:method];
|
|
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
|
|
|
|
// Prepare body
|
|
NSString* bodyString = @"";
|
|
if (data) {
|
|
NSError* error = nil;
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
|
|
if (error) {
|
|
return @{@"error": [error localizedDescription]};
|
|
}
|
|
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
[request setHTTPBody:jsonData];
|
|
}
|
|
|
|
// Generate timestamp and signature
|
|
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
|
NSString* signature = UNComputeSignature(_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
|
|
returningResponse:&response
|
|
error:&error];
|
|
|
|
if (error) {
|
|
return @{@"error": [error localizedDescription]};
|
|
}
|
|
|
|
if ([response statusCode] != 200 && [response statusCode] != 201) {
|
|
NSString* errMsg = responseData ? [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] : @"Unknown error";
|
|
return @{@"error": errMsg, @"status_code": @([response statusCode])};
|
|
}
|
|
|
|
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
|
|
if (error) {
|
|
return @{@"error": [error localizedDescription]};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSDictionary*)apiRequestText:(NSString*)endpoint method:(NSString*)method body:(NSString*)body {
|
|
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlString];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
[request setHTTPMethod:method];
|
|
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
|
|
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
|
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
|
NSString* signature = UNComputeSignature(_secretKey, timestamp, method, endpoint, body);
|
|
|
|
[request setValue:[@"Bearer " stringByAppendingString:_publicKey] forHTTPHeaderField:@"Authorization"];
|
|
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
|
|
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
|
[request setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
|
|
|
|
NSHTTPURLResponse* response = nil;
|
|
NSError* error = nil;
|
|
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
|
|
returningResponse:&response
|
|
error:&error];
|
|
|
|
if (error || ([response statusCode] != 200 && [response statusCode] != 201)) {
|
|
return @{@"error": @"Request failed"};
|
|
}
|
|
|
|
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
|
|
return result ?: @{};
|
|
}
|
|
|
|
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code {
|
|
return [self execute:language code:code options:nil];
|
|
}
|
|
|
|
- (NSDictionary*)execute:(NSString*)language code:(NSString*)code options:(NSDictionary*)options {
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
@"language": language,
|
|
@"code": code,
|
|
@"network_mode": options[@"network_mode"] ?: @"zerotrust",
|
|
@"ttl": options[@"ttl"] ?: @(UN_DEFAULT_TTL),
|
|
@"vcpu": options[@"vcpu"] ?: @1
|
|
}];
|
|
|
|
if (options[@"env"]) payload[@"env"] = options[@"env"];
|
|
if (options[@"input_files"]) payload[@"input_files"] = options[@"input_files"];
|
|
if ([options[@"return_artifact"] boolValue]) payload[@"return_artifact"] = @YES;
|
|
|
|
return [self apiRequest:@"/execute" method:@"POST" data:payload];
|
|
}
|
|
|
|
- (NSDictionary*)executeAsync:(NSString*)language code:(NSString*)code options:(NSDictionary*)options {
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
@"language": language,
|
|
@"code": code,
|
|
@"network_mode": options[@"network_mode"] ?: @"zerotrust",
|
|
@"ttl": options[@"ttl"] ?: @(UN_DEFAULT_TTL),
|
|
@"vcpu": options[@"vcpu"] ?: @1
|
|
}];
|
|
|
|
if (options[@"env"]) payload[@"env"] = options[@"env"];
|
|
if (options[@"input_files"]) payload[@"input_files"] = options[@"input_files"];
|
|
if ([options[@"return_artifact"] boolValue]) payload[@"return_artifact"] = @YES;
|
|
|
|
return [self apiRequest:@"/execute/async" method:@"POST" data:payload];
|
|
}
|
|
|
|
- (NSDictionary*)run:(NSString*)code {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/run?ttl=%ld&network_mode=zerotrust", (long)UN_DEFAULT_TTL];
|
|
return [self apiRequestText:endpoint method:@"POST" body:code];
|
|
}
|
|
|
|
- (NSDictionary*)runAsync:(NSString*)code {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/run/async?ttl=%ld&network_mode=zerotrust", (long)UN_DEFAULT_TTL];
|
|
return [self apiRequestText:endpoint method:@"POST" body:code];
|
|
}
|
|
|
|
- (NSDictionary*)getJob:(NSString*)jobId {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/jobs/%@", jobId];
|
|
return [self apiRequest:endpoint method:@"GET" data:nil];
|
|
}
|
|
|
|
- (NSDictionary*)wait:(NSString*)jobId {
|
|
return [self wait:jobId maxPolls:100];
|
|
}
|
|
|
|
- (NSDictionary*)wait:(NSString*)jobId maxPolls:(int)maxPolls {
|
|
NSSet* terminalStates = [NSSet setWithArray:@[@"completed", @"failed", @"timeout", @"cancelled"]];
|
|
|
|
for (int i = 0; i < maxPolls; i++) {
|
|
int delayIdx = MIN(i, UN_POLL_DELAYS_COUNT - 1);
|
|
usleep(UN_POLL_DELAYS[delayIdx] * 1000); // Convert ms to microseconds
|
|
|
|
NSDictionary* result = [self getJob:jobId];
|
|
NSString* status = result[@"status"];
|
|
|
|
if ([terminalStates containsObject:status]) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return @{@"error": @"Max polls exceeded", @"job_id": jobId};
|
|
}
|
|
|
|
- (NSDictionary*)cancelJob:(NSString*)jobId {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/jobs/%@", jobId];
|
|
return [self apiRequest:endpoint method:@"DELETE" data:nil];
|
|
}
|
|
|
|
- (NSArray*)listJobs {
|
|
NSDictionary* result = [self apiRequest:@"/jobs" method:@"GET" data:nil];
|
|
return result[@"jobs"] ?: @[];
|
|
}
|
|
|
|
- (NSDictionary*)image:(NSString*)prompt {
|
|
return [self image:prompt options:nil];
|
|
}
|
|
|
|
- (NSDictionary*)image:(NSString*)prompt options:(NSDictionary*)options {
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
@"prompt": prompt,
|
|
@"size": options[@"size"] ?: @"1024x1024",
|
|
@"quality": options[@"quality"] ?: @"standard",
|
|
@"n": options[@"n"] ?: @1
|
|
}];
|
|
|
|
if (options[@"model"]) payload[@"model"] = options[@"model"];
|
|
|
|
return [self apiRequest:@"/image" method:@"POST" data:payload];
|
|
}
|
|
|
|
- (NSDictionary*)languages {
|
|
// Check cache first
|
|
if (UNIsCacheValid()) {
|
|
NSDictionary* cached = UNReadLanguagesCache();
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
}
|
|
|
|
// Fetch from API
|
|
NSDictionary* result = [self apiRequest:@"/languages" method:@"GET" data:nil];
|
|
|
|
// Cache result (only if successful)
|
|
if (result && !result[@"error"]) {
|
|
UNWriteLanguagesCache(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
@end
|
|
|
|
// ============================================================================
|
|
// Standalone Library Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Execute code synchronously (standalone function).
|
|
* Uses credentials from environment or config file.
|
|
*/
|
|
NSDictionary* UNExecute(NSString* language, NSString* code, NSDictionary* options) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client execute:language code:code options:options];
|
|
}
|
|
|
|
/**
|
|
* Execute code asynchronously (standalone function).
|
|
*/
|
|
NSDictionary* UNExecuteAsync(NSString* language, NSString* code, NSDictionary* options) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client executeAsync:language code:code options:options];
|
|
}
|
|
|
|
/**
|
|
* Execute with auto-detect (standalone function).
|
|
*/
|
|
NSDictionary* UNRun(NSString* code) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client run:code];
|
|
}
|
|
|
|
/**
|
|
* Execute async with auto-detect (standalone function).
|
|
*/
|
|
NSDictionary* UNRunAsync(NSString* code) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client runAsync:code];
|
|
}
|
|
|
|
/**
|
|
* Get job status (standalone function).
|
|
*/
|
|
NSDictionary* UNGetJob(NSString* jobId) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client getJob:jobId];
|
|
}
|
|
|
|
/**
|
|
* Wait for job completion (standalone function).
|
|
*/
|
|
NSDictionary* UNWait(NSString* jobId) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client wait:jobId];
|
|
}
|
|
|
|
/**
|
|
* Cancel a job (standalone function).
|
|
*/
|
|
NSDictionary* UNCancelJob(NSString* jobId) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client cancelJob:jobId];
|
|
}
|
|
|
|
/**
|
|
* List active jobs (standalone function).
|
|
*/
|
|
NSArray* UNListJobs(void) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client listJobs];
|
|
}
|
|
|
|
/**
|
|
* Generate image (standalone function).
|
|
*/
|
|
NSDictionary* UNImage(NSString* prompt, NSDictionary* options) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client image:prompt options:options];
|
|
}
|
|
|
|
/**
|
|
* Get supported languages (standalone function).
|
|
* Results are cached for 1 hour in ~/.unsandbox/languages.json
|
|
*/
|
|
NSDictionary* UNLanguages(void) {
|
|
UNClient* client = [[UNClient alloc] init];
|
|
return [client languages];
|
|
}
|
|
|
|
// ============================================================================
|
|
// CLI Helper Functions
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
// Sudo OTP Challenge Handling
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Handle 428 sudo OTP challenge - prompts user for OTP and retries the request.
|
|
*
|
|
* @param responseData The JSON response containing challenge_id
|
|
* @param meth HTTP method (DELETE, POST, etc.)
|
|
* @param endpoint API endpoint path
|
|
* @param body Request body (or nil for DELETE)
|
|
* @param publicKey API public key
|
|
* @param secretKey API secret key
|
|
* @return YES on success, NO on failure
|
|
*/
|
|
BOOL handleSudoChallenge(NSDictionary* responseData, NSString* meth, NSString* endpoint, NSString* body, NSString* publicKey, NSString* secretKey) {
|
|
NSString* challengeId = responseData[@"challenge_id"];
|
|
|
|
fprintf(stderr, "%sConfirmation required. Check your email for a one-time code.%s\n",
|
|
[YELLOW UTF8String], [RESET UTF8String]);
|
|
fprintf(stderr, "Enter OTP: ");
|
|
|
|
char otpBuffer[64];
|
|
if (!fgets(otpBuffer, sizeof(otpBuffer), stdin)) {
|
|
fprintf(stderr, "%sError: Failed to read OTP%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
return NO;
|
|
}
|
|
|
|
// Strip newline
|
|
NSString* otp = [[NSString stringWithUTF8String:otpBuffer] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
|
|
if ([otp length] == 0) {
|
|
fprintf(stderr, "%sError: Operation cancelled%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
return NO;
|
|
}
|
|
|
|
// Build retry request with sudo headers
|
|
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlString];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
[request setHTTPMethod:meth];
|
|
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
|
|
|
|
NSString* bodyString = body ?: @"";
|
|
if (body && [body length] > 0) {
|
|
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
|
|
}
|
|
|
|
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
|
NSString* signature = UNComputeSignature(secretKey, timestamp, meth, endpoint, bodyString);
|
|
|
|
[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"];
|
|
[request setValue:otp forHTTPHeaderField:@"X-Sudo-OTP"];
|
|
if (challengeId) {
|
|
[request setValue:challengeId forHTTPHeaderField:@"X-Sudo-Challenge"];
|
|
}
|
|
|
|
NSHTTPURLResponse* response = nil;
|
|
NSError* error = nil;
|
|
NSData* responseData2 = [NSURLConnection sendSynchronousRequest:request
|
|
returningResponse:&response
|
|
error:&error];
|
|
|
|
if (error || [response statusCode] < 200 || [response statusCode] >= 300) {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n",
|
|
[RED UTF8String], (long)[response statusCode], [RESET UTF8String]);
|
|
if (responseData2) {
|
|
NSString* errMsg = [[NSString alloc] initWithData:responseData2 encoding:NSUTF8StringEncoding];
|
|
fprintf(stderr, "%s\n", [errMsg UTF8String]);
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
fprintf(stderr, "%sOperation completed successfully%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
return YES;
|
|
}
|
|
|
|
/**
|
|
* Make an API request that returns status code and response (for 428 handling).
|
|
*/
|
|
NSDictionary* apiRequestWithStatusCLI(NSString* endpoint, NSString* method, NSDictionary* data, NSString* publicKey, NSString* secretKey, NSInteger* outStatusCode) {
|
|
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlString];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
[request setHTTPMethod:method];
|
|
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
|
|
|
|
NSString* bodyString = @"";
|
|
if (data) {
|
|
NSError* error = nil;
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
|
|
if (error) {
|
|
*outStatusCode = 500;
|
|
return @{@"error": [error localizedDescription]};
|
|
}
|
|
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
[request setHTTPBody:jsonData];
|
|
}
|
|
|
|
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
|
NSString* signature = UNComputeSignature(secretKey, timestamp, method, endpoint, bodyString);
|
|
|
|
[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
|
|
returningResponse:&response
|
|
error:&error];
|
|
|
|
*outStatusCode = [response statusCode];
|
|
|
|
if (error) {
|
|
return @{@"error": [error localizedDescription]};
|
|
}
|
|
|
|
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
|
|
return result ?: @{};
|
|
}
|
|
|
|
NSDictionary* apiRequestCLI(NSString* endpoint, NSString* method, NSDictionary* data, NSString* publicKey, NSString* secretKey) {
|
|
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlString];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
[request setHTTPMethod:method];
|
|
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
|
|
|
|
NSString* bodyString = @"";
|
|
if (data) {
|
|
NSError* error = nil;
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error];
|
|
if (error) {
|
|
fprintf(stderr, "%sError creating JSON: %s%s\n",
|
|
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
bodyString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
[request setHTTPBody:jsonData];
|
|
}
|
|
|
|
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
|
NSString* signature = UNComputeSignature(secretKey, timestamp, method, endpoint, bodyString);
|
|
|
|
[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
|
|
returningResponse:&response
|
|
error:&error];
|
|
|
|
if (error || ([response statusCode] != 200 && [response statusCode] != 201)) {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n",
|
|
[RED UTF8String], (long)[response statusCode], [RESET UTF8String]);
|
|
if (responseData) {
|
|
NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
|
|
fprintf(stderr, "%s\n", [errMsg UTF8String]);
|
|
UNCheckClockDrift(errMsg);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
|
|
if (error) {
|
|
fprintf(stderr, "%sError parsing JSON: %s%s\n",
|
|
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
NSDictionary* apiRequestPutTextCLI(NSString* endpoint, NSString* content, NSString* publicKey, NSString* secretKey) {
|
|
NSString* urlString = [UN_API_BASE stringByAppendingString:endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlString];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
[request setHTTPMethod:@"PUT"];
|
|
[request setTimeoutInterval:UN_DEFAULT_TIMEOUT];
|
|
[request setHTTPBody:[content dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
|
long timestamp = (long)[[NSDate date] timeIntervalSince1970];
|
|
NSString* signature = UNComputeSignature(secretKey, timestamp, @"PUT", endpoint, content);
|
|
|
|
[request setValue:[@"Bearer " stringByAppendingString:publicKey] forHTTPHeaderField:@"Authorization"];
|
|
[request setValue:[NSString stringWithFormat:@"%ld", timestamp] forHTTPHeaderField:@"X-Timestamp"];
|
|
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
|
[request setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];
|
|
|
|
NSHTTPURLResponse* response = nil;
|
|
NSError* error = nil;
|
|
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
|
|
returningResponse:&response
|
|
error:&error];
|
|
|
|
if (error || ([response statusCode] != 200 && [response statusCode] != 201)) {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n",
|
|
[RED UTF8String], (long)[response statusCode], [RESET UTF8String]);
|
|
if (responseData) {
|
|
NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
|
|
fprintf(stderr, "%s\n", [errMsg UTF8String]);
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
NSDictionary* result = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
|
|
return result;
|
|
}
|
|
|
|
NSString* buildEnvContent(NSArray* envVars, NSString* envFile) {
|
|
NSMutableArray* lines = [NSMutableArray array];
|
|
for (NSString* var in envVars) {
|
|
[lines addObject:var];
|
|
}
|
|
if (envFile && [[NSFileManager defaultManager] fileExistsAtPath:envFile]) {
|
|
NSString* fileContent = [NSString stringWithContentsOfFile:envFile encoding:NSUTF8StringEncoding error:nil];
|
|
for (NSString* line in [fileContent componentsSeparatedByString:@"\n"]) {
|
|
NSString* trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
if ([trimmed length] == 0 || [trimmed hasPrefix:@"#"]) continue;
|
|
[lines addObject:line];
|
|
}
|
|
}
|
|
return [lines componentsJoinedByString:@"\n"];
|
|
}
|
|
|
|
// Service vault functions
|
|
void serviceEnvStatus(NSString* serviceId, NSString* publicKey, NSString* secretKey) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env", serviceId];
|
|
NSDictionary* result = apiRequestCLI(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]);
|
|
}
|
|
|
|
void serviceEnvSet(NSString* serviceId, NSString* content, NSString* publicKey, NSString* secretKey) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env", serviceId];
|
|
NSDictionary* result = apiRequestPutTextCLI(endpoint, content, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
}
|
|
|
|
void serviceEnvExport(NSString* serviceId, NSString* publicKey, NSString* secretKey) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env/export", serviceId];
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
|
if (result[@"content"]) {
|
|
printf("%s", [result[@"content"] UTF8String]);
|
|
}
|
|
}
|
|
|
|
void serviceEnvDelete(NSString* serviceId, NSString* publicKey, NSString* secretKey) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/env", serviceId];
|
|
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
|
|
printf("%sVault deleted for: %s%s\n", [GREEN UTF8String], [serviceId UTF8String], [RESET UTF8String]);
|
|
}
|
|
|
|
// ============================================================================
|
|
// CLI Commands
|
|
// ============================================================================
|
|
|
|
void cmdExecute(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
NSString* sourceFile = nil;
|
|
NSMutableDictionary* envVars = [NSMutableDictionary dictionary];
|
|
NSMutableArray* inputFiles = [NSMutableArray array];
|
|
BOOL artifacts = NO;
|
|
NSString* outputDir = @".";
|
|
NSString* network = nil;
|
|
int vcpu = 0;
|
|
|
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if ([arg isEqualToString:@"-e"] && i + 1 < [args count]) {
|
|
NSArray* parts = [args[++i] componentsSeparatedByString:@"="];
|
|
if ([parts count] >= 2) {
|
|
envVars[parts[0]] = [[parts subarrayWithRange:NSMakeRange(1, [parts count] - 1)] componentsJoinedByString:@"="];
|
|
}
|
|
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
|
[inputFiles addObject:args[++i]];
|
|
} else if ([arg isEqualToString:@"-a"]) {
|
|
artifacts = YES;
|
|
} else if ([arg isEqualToString:@"-o"] && i + 1 < [args count]) {
|
|
outputDir = args[++i];
|
|
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
|
|
network = args[++i];
|
|
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
|
|
vcpu = [args[++i] intValue];
|
|
} else if (![arg hasPrefix:@"-"]) {
|
|
sourceFile = arg;
|
|
}
|
|
}
|
|
|
|
if (!sourceFile) {
|
|
fprintf(stderr, "Usage: un.m [options] <source_file>\n");
|
|
exit(1);
|
|
}
|
|
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
if (![fm fileExistsAtPath:sourceFile]) {
|
|
fprintf(stderr, "%sError: File not found: %s%s\n",
|
|
[RED UTF8String], [sourceFile UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
NSError* error = nil;
|
|
NSString* code = [NSString stringWithContentsOfFile:sourceFile encoding:NSUTF8StringEncoding error:&error];
|
|
if (error) {
|
|
fprintf(stderr, "%sError reading file: %s%s\n",
|
|
[RED UTF8String], [[error localizedDescription] UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
NSString* language = UNDetectLanguage(sourceFile);
|
|
if (!language) {
|
|
fprintf(stderr, "%sError: Cannot detect language for %s%s\n",
|
|
[RED UTF8String], [sourceFile UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
@"language": language,
|
|
@"code": code
|
|
}];
|
|
|
|
if ([envVars count] > 0) {
|
|
payload[@"env"] = envVars;
|
|
}
|
|
|
|
if ([inputFiles count] > 0) {
|
|
NSMutableArray* files = [NSMutableArray array];
|
|
for (NSString* filepath in inputFiles) {
|
|
if (![fm fileExistsAtPath:filepath]) {
|
|
fprintf(stderr, "%sError: Input file not found: %s%s\n",
|
|
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSData* content = [NSData dataWithContentsOfFile:filepath];
|
|
NSString* b64Content = [content base64EncodedStringWithOptions:0];
|
|
[files addObject:@{
|
|
@"filename": [filepath lastPathComponent],
|
|
@"content_base64": b64Content
|
|
}];
|
|
}
|
|
payload[@"input_files"] = files;
|
|
}
|
|
|
|
if (artifacts) payload[@"return_artifacts"] = @YES;
|
|
if (network) payload[@"network"] = network;
|
|
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
|
|
|
NSDictionary* result = apiRequestCLI(@"/execute", @"POST", payload, publicKey, secretKey);
|
|
|
|
NSString* stdoutText = result[@"stdout"] ?: @"";
|
|
NSString* stderrText = result[@"stderr"] ?: @"";
|
|
|
|
if ([stdoutText length] > 0) {
|
|
printf("%s%s%s", [BLUE UTF8String], [stdoutText UTF8String], [RESET UTF8String]);
|
|
}
|
|
if ([stderrText length] > 0) {
|
|
fprintf(stderr, "%s%s%s", [RED UTF8String], [stderrText UTF8String], [RESET UTF8String]);
|
|
}
|
|
|
|
if (artifacts && result[@"artifacts"]) {
|
|
[fm createDirectoryAtPath:outputDir withIntermediateDirectories:YES attributes:nil error:nil];
|
|
for (NSDictionary* artifact in result[@"artifacts"]) {
|
|
NSString* filename = artifact[@"filename"];
|
|
NSString* b64Content = artifact[@"content_base64"];
|
|
NSData* content = [[NSData alloc] initWithBase64EncodedString:b64Content options:0];
|
|
NSString* path = [outputDir stringByAppendingPathComponent:filename];
|
|
[content writeToFile:path atomically:YES];
|
|
[fm setAttributes:@{NSFilePosixPermissions: @0755} ofItemAtPath:path error:nil];
|
|
fprintf(stderr, "%sSaved: %s%s\n", [GREEN UTF8String], [path UTF8String], [RESET UTF8String]);
|
|
}
|
|
}
|
|
|
|
int exitCode = [result[@"exit_code"] intValue];
|
|
exit(exitCode);
|
|
}
|
|
|
|
void cmdSession(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
BOOL listMode = NO;
|
|
NSString* killId = nil;
|
|
NSString* shell = nil;
|
|
NSString* network = nil;
|
|
int vcpu = 0;
|
|
NSMutableArray* inputFiles = [NSMutableArray array];
|
|
|
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if ([arg isEqualToString:@"--list"]) {
|
|
listMode = YES;
|
|
} else if ([arg isEqualToString:@"--kill"] && i + 1 < [args count]) {
|
|
killId = args[++i];
|
|
} else if ([arg isEqualToString:@"--shell"] && i + 1 < [args count]) {
|
|
shell = args[++i];
|
|
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
|
[inputFiles addObject:args[++i]];
|
|
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
|
|
network = args[++i];
|
|
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
|
|
vcpu = [args[++i] intValue];
|
|
}
|
|
}
|
|
|
|
if (listMode) {
|
|
NSDictionary* result = apiRequestCLI(@"/sessions", @"GET", nil, publicKey, secretKey);
|
|
NSArray* sessions = result[@"sessions"];
|
|
if ([sessions count] == 0) {
|
|
printf("No active sessions\n");
|
|
} else {
|
|
printf("%-40s %-10s %-10s %s\n", "ID", "Shell", "Status", "Created");
|
|
for (NSDictionary* s in sessions) {
|
|
printf("%-40s %-10s %-10s %s\n",
|
|
[s[@"id"] UTF8String],
|
|
[s[@"shell"] UTF8String],
|
|
[s[@"status"] UTF8String],
|
|
[s[@"created_at"] UTF8String]);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (killId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@", killId];
|
|
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
|
|
printf("%sSession terminated: %s%s\n", [GREEN UTF8String], [killId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
@"shell": shell ?: @"bash"
|
|
}];
|
|
if (network) payload[@"network"] = network;
|
|
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
|
|
|
if ([inputFiles count] > 0) {
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
NSMutableArray* files = [NSMutableArray array];
|
|
for (NSString* filepath in inputFiles) {
|
|
if (![fm fileExistsAtPath:filepath]) {
|
|
fprintf(stderr, "%sError: Input file not found: %s%s\n",
|
|
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSData* content = [NSData dataWithContentsOfFile:filepath];
|
|
NSString* b64Content = [content base64EncodedStringWithOptions:0];
|
|
[files addObject:@{
|
|
@"filename": [filepath lastPathComponent],
|
|
@"content_base64": b64Content
|
|
}];
|
|
}
|
|
payload[@"input_files"] = files;
|
|
}
|
|
|
|
printf("%sCreating session...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
|
|
NSDictionary* result = apiRequestCLI(@"/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]);
|
|
}
|
|
|
|
/**
|
|
* Set unfreeze_on_demand flag for a service.
|
|
*/
|
|
void setServiceUnfreezeOnDemand(NSString* serviceId, BOOL enabled, NSString* publicKey, NSString* secretKey) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", serviceId];
|
|
NSDictionary* payload = @{@"unfreeze_on_demand": @(enabled)};
|
|
apiRequestCLI(endpoint, @"PATCH", payload, publicKey, secretKey);
|
|
}
|
|
|
|
void cmdService(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
BOOL listMode = NO;
|
|
NSString* infoId = nil;
|
|
NSString* logsId = nil;
|
|
NSString* sleepId = nil;
|
|
NSString* wakeId = nil;
|
|
NSString* destroyId = nil;
|
|
NSString* name = nil;
|
|
NSString* ports = nil;
|
|
NSString* type = nil;
|
|
NSString* bootstrap = nil;
|
|
NSString* bootstrapFile = nil;
|
|
NSString* network = nil;
|
|
int vcpu = 0;
|
|
BOOL unfreezeOnDemand = NO;
|
|
NSString* setUnfreezeOnDemandId = nil;
|
|
BOOL setUnfreezeOnDemandEnabled = NO;
|
|
NSMutableArray* inputFiles = [NSMutableArray array];
|
|
NSMutableArray* envVars = [NSMutableArray array];
|
|
NSString* envFile = nil;
|
|
|
|
// Check for 'env' subcommand first
|
|
if ([args count] >= 1 && [args[0] isEqualToString:@"env"]) {
|
|
if ([args count] < 3) {
|
|
fprintf(stderr, "Usage: un.m service env <status|set|export|delete> <service_id> [options]\n");
|
|
exit(1);
|
|
}
|
|
NSString* envAction = args[1];
|
|
NSString* envTarget = args[2];
|
|
|
|
for (NSUInteger i = 3; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if ([arg isEqualToString:@"-e"] && i + 1 < [args count]) {
|
|
[envVars addObject:args[++i]];
|
|
} else if ([arg isEqualToString:@"--env-file"] && i + 1 < [args count]) {
|
|
envFile = args[++i];
|
|
}
|
|
}
|
|
|
|
if ([envAction isEqualToString:@"status"]) {
|
|
serviceEnvStatus(envTarget, publicKey, secretKey);
|
|
} else if ([envAction isEqualToString:@"set"]) {
|
|
NSString* content = buildEnvContent(envVars, envFile);
|
|
if ([content length] == 0) {
|
|
fprintf(stderr, "%sError: No environment variables to set%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
serviceEnvSet(envTarget, content, publicKey, secretKey);
|
|
} else if ([envAction isEqualToString:@"export"]) {
|
|
serviceEnvExport(envTarget, publicKey, secretKey);
|
|
} else if ([envAction isEqualToString:@"delete"]) {
|
|
serviceEnvDelete(envTarget, publicKey, secretKey);
|
|
} else {
|
|
fprintf(stderr, "%sError: Unknown env action '%s'. Use status, set, export, or delete%s\n",
|
|
[RED UTF8String], [envAction UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if ([arg isEqualToString:@"--list"]) {
|
|
listMode = YES;
|
|
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
|
|
infoId = args[++i];
|
|
} else if ([arg isEqualToString:@"--logs"] && i + 1 < [args count]) {
|
|
logsId = args[++i];
|
|
} else if ([arg isEqualToString:@"--freeze"] && i + 1 < [args count]) {
|
|
sleepId = args[++i];
|
|
} else if ([arg isEqualToString:@"--unfreeze"] && i + 1 < [args count]) {
|
|
wakeId = args[++i];
|
|
} else if ([arg isEqualToString:@"--destroy"] && i + 1 < [args count]) {
|
|
destroyId = args[++i];
|
|
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
|
name = args[++i];
|
|
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
|
ports = args[++i];
|
|
} else if ([arg isEqualToString:@"--type"] && i + 1 < [args count]) {
|
|
type = args[++i];
|
|
} else if ([arg isEqualToString:@"--bootstrap"] && i + 1 < [args count]) {
|
|
bootstrap = args[++i];
|
|
} else if ([arg isEqualToString:@"--bootstrap-file"] && i + 1 < [args count]) {
|
|
bootstrapFile = args[++i];
|
|
} else if ([arg isEqualToString:@"-f"] && i + 1 < [args count]) {
|
|
[inputFiles addObject:args[++i]];
|
|
} else if ([arg isEqualToString:@"-e"] && i + 1 < [args count]) {
|
|
[envVars addObject:args[++i]];
|
|
} else if ([arg isEqualToString:@"--env-file"] && i + 1 < [args count]) {
|
|
envFile = args[++i];
|
|
} else if ([arg isEqualToString:@"-n"] && i + 1 < [args count]) {
|
|
network = args[++i];
|
|
} else if ([arg isEqualToString:@"-v"] && i + 1 < [args count]) {
|
|
vcpu = [args[++i] intValue];
|
|
} else if ([arg isEqualToString:@"--unfreeze-on-demand"]) {
|
|
unfreezeOnDemand = YES;
|
|
} else if ([arg isEqualToString:@"--set-unfreeze-on-demand"] && i + 2 < [args count]) {
|
|
setUnfreezeOnDemandId = args[++i];
|
|
NSString* enabledStr = args[++i];
|
|
setUnfreezeOnDemandEnabled = [enabledStr isEqualToString:@"true"] || [enabledStr isEqualToString:@"1"];
|
|
}
|
|
}
|
|
|
|
if (setUnfreezeOnDemandId) {
|
|
setServiceUnfreezeOnDemand(setUnfreezeOnDemandId, setUnfreezeOnDemandEnabled, publicKey, secretKey);
|
|
NSString* status = setUnfreezeOnDemandEnabled ? @"enabled" : @"disabled";
|
|
printf("%sUnfreeze-on-demand %s for service: %s%s\n",
|
|
[GREEN UTF8String], [status UTF8String], [setUnfreezeOnDemandId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (listMode) {
|
|
NSDictionary* result = apiRequestCLI(@"/services", @"GET", nil, publicKey, secretKey);
|
|
NSArray* services = result[@"services"];
|
|
if ([services count] == 0) {
|
|
printf("No services\n");
|
|
} else {
|
|
printf("%-20s %-15s %-10s %-15s %s\n", "ID", "Name", "Status", "Ports", "Domains");
|
|
for (NSDictionary* s in services) {
|
|
NSArray* portArray = s[@"ports"];
|
|
NSArray* domainArray = s[@"domains"];
|
|
NSString* portStr = [portArray componentsJoinedByString:@","];
|
|
NSString* domainStr = [domainArray componentsJoinedByString:@","];
|
|
printf("%-20s %-15s %-10s %-15s %s\n",
|
|
[s[@"id"] UTF8String],
|
|
[s[@"name"] UTF8String],
|
|
[s[@"status"] UTF8String],
|
|
[portStr UTF8String],
|
|
[domainStr UTF8String]);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (infoId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", infoId];
|
|
NSDictionary* result = apiRequestCLI(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]);
|
|
return;
|
|
}
|
|
|
|
if (logsId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/logs", logsId];
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"GET", nil, publicKey, secretKey);
|
|
printf("%s", [result[@"logs"] UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (sleepId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/freeze", sleepId];
|
|
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
|
printf("%sService frozen: %s%s\n", [GREEN UTF8String], [sleepId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (wakeId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/unfreeze", wakeId];
|
|
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
|
printf("%sService unfreezing: %s%s\n", [GREEN UTF8String], [wakeId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (destroyId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", destroyId];
|
|
NSInteger statusCode = 0;
|
|
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
|
|
|
|
if (statusCode == 428) {
|
|
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
|
|
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
exit(1);
|
|
}
|
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (name) {
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{@"name": name}];
|
|
|
|
if (ports) {
|
|
NSArray* portStrings = [ports componentsSeparatedByString:@","];
|
|
NSMutableArray* portNumbers = [NSMutableArray array];
|
|
for (NSString* p in portStrings) {
|
|
[portNumbers addObject:@([p intValue])];
|
|
}
|
|
payload[@"ports"] = portNumbers;
|
|
}
|
|
|
|
if (type) payload[@"service_type"] = type;
|
|
if (bootstrap) payload[@"bootstrap"] = bootstrap;
|
|
|
|
if (bootstrapFile) {
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
if ([fm fileExistsAtPath:bootstrapFile]) {
|
|
NSString* content = [NSString stringWithContentsOfFile:bootstrapFile encoding:NSUTF8StringEncoding error:nil];
|
|
payload[@"bootstrap_content"] = content;
|
|
} else {
|
|
fprintf(stderr, "%sError: Bootstrap file not found: %s%s\n",
|
|
[RED UTF8String], [bootstrapFile UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
if ([inputFiles count] > 0) {
|
|
NSFileManager* fm = [NSFileManager defaultManager];
|
|
NSMutableArray* files = [NSMutableArray array];
|
|
for (NSString* filepath in inputFiles) {
|
|
if (![fm fileExistsAtPath:filepath]) {
|
|
fprintf(stderr, "%sError: Input file not found: %s%s\n",
|
|
[RED UTF8String], [filepath UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSData* content = [NSData dataWithContentsOfFile:filepath];
|
|
NSString* b64Content = [content base64EncodedStringWithOptions:0];
|
|
[files addObject:@{
|
|
@"filename": [filepath lastPathComponent],
|
|
@"content_base64": b64Content
|
|
}];
|
|
}
|
|
payload[@"input_files"] = files;
|
|
}
|
|
|
|
if (network) payload[@"network"] = network;
|
|
if (vcpu > 0) payload[@"vcpu"] = @(vcpu);
|
|
if (unfreezeOnDemand) payload[@"unfreeze_on_demand"] = @YES;
|
|
|
|
NSDictionary* result = apiRequestCLI(@"/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"]) {
|
|
printf("URL: %s\n", [result[@"url"] UTF8String]);
|
|
}
|
|
|
|
NSString* envContent = buildEnvContent(envVars, envFile);
|
|
if ([envContent length] > 0 && result[@"id"]) {
|
|
printf("%sSetting vault for service...%s\n", [YELLOW UTF8String], [RESET UTF8String]);
|
|
serviceEnvSet(result[@"id"], envContent, publicKey, secretKey);
|
|
}
|
|
return;
|
|
}
|
|
|
|
fprintf(stderr, "%sError: Specify --name to create a service, or use --list, --info, env, etc.%s\n",
|
|
[RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
void cmdKey(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
|
|
printf("%sValid%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("Public Key: %s\n", [publicKey UTF8String]);
|
|
}
|
|
|
|
void cmdImage(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
|
|
BOOL listMode = NO;
|
|
NSString* infoId = nil;
|
|
NSString* deleteId = nil;
|
|
NSString* lockId = nil;
|
|
NSString* unlockId = nil;
|
|
NSString* publishId = nil;
|
|
NSString* sourceType = nil;
|
|
NSString* visibilityId = nil;
|
|
NSString* visibilityMode = nil;
|
|
NSString* spawnId = nil;
|
|
NSString* cloneId = nil;
|
|
NSString* name = nil;
|
|
NSString* ports = nil;
|
|
NSString* grantId = nil;
|
|
NSString* revokeId = nil;
|
|
NSString* trustedId = nil;
|
|
NSString* trustedKey = nil;
|
|
NSString* transferId = nil;
|
|
NSString* toKey = nil;
|
|
|
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) {
|
|
listMode = YES;
|
|
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
|
|
infoId = args[++i];
|
|
} else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) {
|
|
deleteId = args[++i];
|
|
} else if ([arg isEqualToString:@"--lock"] && i + 1 < [args count]) {
|
|
lockId = args[++i];
|
|
} else if ([arg isEqualToString:@"--unlock"] && i + 1 < [args count]) {
|
|
unlockId = args[++i];
|
|
} else if ([arg isEqualToString:@"--publish"] && i + 1 < [args count]) {
|
|
publishId = args[++i];
|
|
} else if ([arg isEqualToString:@"--source-type"] && i + 1 < [args count]) {
|
|
sourceType = args[++i];
|
|
} else if ([arg isEqualToString:@"--visibility"] && i + 2 < [args count]) {
|
|
visibilityId = args[++i];
|
|
visibilityMode = args[++i];
|
|
} else if ([arg isEqualToString:@"--spawn"] && i + 1 < [args count]) {
|
|
spawnId = args[++i];
|
|
} else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) {
|
|
cloneId = args[++i];
|
|
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
|
name = args[++i];
|
|
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
|
ports = args[++i];
|
|
} else if ([arg isEqualToString:@"--grant"] && i + 1 < [args count]) {
|
|
grantId = args[++i];
|
|
} else if ([arg isEqualToString:@"--revoke"] && i + 1 < [args count]) {
|
|
revokeId = args[++i];
|
|
} else if ([arg isEqualToString:@"--trusted"] && i + 1 < [args count]) {
|
|
trustedId = args[++i];
|
|
} else if ([arg isEqualToString:@"--trusted-key"] && i + 1 < [args count]) {
|
|
trustedKey = args[++i];
|
|
} else if ([arg isEqualToString:@"--transfer"] && i + 1 < [args count]) {
|
|
transferId = args[++i];
|
|
} else if ([arg isEqualToString:@"--to-key"] && i + 1 < [args count]) {
|
|
toKey = args[++i];
|
|
}
|
|
}
|
|
|
|
if (listMode) {
|
|
NSDictionary* result = apiRequestCLI(@"/images", @"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]);
|
|
return;
|
|
}
|
|
|
|
if (infoId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@", infoId];
|
|
NSDictionary* result = apiRequestCLI(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]);
|
|
return;
|
|
}
|
|
|
|
if (deleteId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@", deleteId];
|
|
NSInteger statusCode = 0;
|
|
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
|
|
|
|
if (statusCode == 428) {
|
|
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
|
|
printf("%sImage deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
exit(1);
|
|
}
|
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
printf("%sImage deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (lockId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/lock", lockId];
|
|
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
|
printf("%sImage locked: %s%s\n", [GREEN UTF8String], [lockId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (unlockId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/unlock", unlockId];
|
|
NSInteger statusCode = 0;
|
|
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", @{}, publicKey, secretKey, &statusCode);
|
|
|
|
if (statusCode == 428) {
|
|
if (handleSudoChallenge(response, @"POST", endpoint, @"{}", publicKey, secretKey)) {
|
|
printf("%sImage unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
exit(1);
|
|
}
|
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
printf("%sImage unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (publishId) {
|
|
if (!sourceType) {
|
|
fprintf(stderr, "%sError: --publish requires --source-type (service or snapshot)%s\n",
|
|
[RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionaryWithDictionary:@{
|
|
@"source_type": sourceType,
|
|
@"source_id": publishId
|
|
}];
|
|
if (name) payload[@"name"] = name;
|
|
|
|
NSDictionary* result = apiRequestCLI(@"/images/publish", @"POST", payload, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sImage published%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (visibilityId && visibilityMode) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/visibility", visibilityId];
|
|
NSDictionary* payload = @{@"visibility": visibilityMode};
|
|
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
printf("%sImage visibility set to %s: %s%s\n", [GREEN UTF8String], [visibilityMode UTF8String], [visibilityId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (spawnId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/spawn", spawnId];
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
|
if (name) payload[@"name"] = name;
|
|
if (ports) {
|
|
NSArray* portStrings = [ports componentsSeparatedByString:@","];
|
|
NSMutableArray* portNumbers = [NSMutableArray array];
|
|
for (NSString* p in portStrings) {
|
|
[portNumbers addObject:@([p intValue])];
|
|
}
|
|
payload[@"ports"] = portNumbers;
|
|
}
|
|
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sService spawned from image%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (cloneId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/clone", cloneId];
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
|
if (name) payload[@"name"] = name;
|
|
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sImage cloned%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (grantId) {
|
|
if (!trustedKey) {
|
|
fprintf(stderr, "%sError: --grant requires --trusted-key%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/grant", grantId];
|
|
NSDictionary* payload = @{@"trusted_api_key": trustedKey};
|
|
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
printf("%sAccess granted to %s%s\n", [GREEN UTF8String], [trustedKey UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (revokeId) {
|
|
if (!trustedKey) {
|
|
fprintf(stderr, "%sError: --revoke requires --trusted-key%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/revoke", revokeId];
|
|
NSDictionary* payload = @{@"trusted_api_key": trustedKey};
|
|
apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
printf("%sAccess revoked from %s%s\n", [GREEN UTF8String], [trustedKey UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (trustedId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/trusted", trustedId];
|
|
NSDictionary* result = apiRequestCLI(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]);
|
|
return;
|
|
}
|
|
|
|
if (transferId) {
|
|
if (!toKey) {
|
|
fprintf(stderr, "%sError: --transfer requires --to-key%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/transfer", transferId];
|
|
NSDictionary* payload = @{@"to_api_key": toKey};
|
|
NSInteger statusCode = 0;
|
|
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", payload, publicKey, secretKey, &statusCode);
|
|
|
|
if (statusCode == 428) {
|
|
NSData* bodyData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil];
|
|
NSString* bodyStr = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
|
|
if (handleSudoChallenge(response, @"POST", endpoint, bodyStr, publicKey, secretKey)) {
|
|
printf("%sImage transferred to %s%s\n", [GREEN UTF8String], [toKey UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
exit(1);
|
|
}
|
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
printf("%sImage transferred to %s%s\n", [GREEN UTF8String], [toKey UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
fprintf(stderr, "%sError: Use --list, --info, --delete, --lock, --unlock, --publish, --visibility, --spawn, --clone, --grant, --revoke, --trusted, or --transfer%s\n",
|
|
[RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Snapshot Command
|
|
// ============================================================================
|
|
|
|
void cmdSnapshot(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
|
|
BOOL listMode = NO;
|
|
NSString* infoId = nil;
|
|
NSString* sessionId = nil;
|
|
NSString* serviceId = nil;
|
|
NSString* restoreId = nil;
|
|
NSString* deleteId = nil;
|
|
NSString* lockId = nil;
|
|
NSString* unlockId = nil;
|
|
NSString* cloneId = nil;
|
|
NSString* cloneType = nil;
|
|
NSString* name = nil;
|
|
NSString* ports = nil;
|
|
BOOL hot = NO;
|
|
|
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if ([arg isEqualToString:@"--list"] || [arg isEqualToString:@"-l"]) {
|
|
listMode = YES;
|
|
} else if ([arg isEqualToString:@"--info"] && i + 1 < [args count]) {
|
|
infoId = args[++i];
|
|
} else if ([arg isEqualToString:@"--session"] && i + 1 < [args count]) {
|
|
sessionId = args[++i];
|
|
} else if ([arg isEqualToString:@"--service"] && i + 1 < [args count]) {
|
|
serviceId = args[++i];
|
|
} else if ([arg isEqualToString:@"--restore"] && i + 1 < [args count]) {
|
|
restoreId = args[++i];
|
|
} else if ([arg isEqualToString:@"--delete"] && i + 1 < [args count]) {
|
|
deleteId = args[++i];
|
|
} else if ([arg isEqualToString:@"--lock"] && i + 1 < [args count]) {
|
|
lockId = args[++i];
|
|
} else if ([arg isEqualToString:@"--unlock"] && i + 1 < [args count]) {
|
|
unlockId = args[++i];
|
|
} else if ([arg isEqualToString:@"--clone"] && i + 1 < [args count]) {
|
|
cloneId = args[++i];
|
|
} else if ([arg isEqualToString:@"--clone-type"] && i + 1 < [args count]) {
|
|
cloneType = args[++i];
|
|
} else if ([arg isEqualToString:@"--name"] && i + 1 < [args count]) {
|
|
name = args[++i];
|
|
} else if ([arg isEqualToString:@"--ports"] && i + 1 < [args count]) {
|
|
ports = args[++i];
|
|
} else if ([arg isEqualToString:@"--hot"]) {
|
|
hot = YES;
|
|
}
|
|
}
|
|
|
|
if (listMode) {
|
|
NSDictionary* result = apiRequestCLI(@"/snapshots", @"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]);
|
|
return;
|
|
}
|
|
|
|
if (infoId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", infoId];
|
|
NSDictionary* result = apiRequestCLI(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]);
|
|
return;
|
|
}
|
|
|
|
if (sessionId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/sessions/%@/snapshot", sessionId];
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
|
if (name) payload[@"name"] = name;
|
|
if (hot) payload[@"hot"] = @YES;
|
|
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sSnapshot created%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (serviceId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/services/%@/snapshot", serviceId];
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
|
if (name) payload[@"name"] = name;
|
|
if (hot) payload[@"hot"] = @YES;
|
|
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sSnapshot created%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (restoreId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/restore", restoreId];
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", @{}, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sSnapshot restored%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (deleteId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@", deleteId];
|
|
NSInteger statusCode = 0;
|
|
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"DELETE", nil, publicKey, secretKey, &statusCode);
|
|
|
|
if (statusCode == 428) {
|
|
if (handleSudoChallenge(response, @"DELETE", endpoint, nil, publicKey, secretKey)) {
|
|
printf("%sSnapshot deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
exit(1);
|
|
}
|
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
printf("%sSnapshot deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (lockId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/lock", lockId];
|
|
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
|
printf("%sSnapshot locked: %s%s\n", [GREEN UTF8String], [lockId UTF8String], [RESET UTF8String]);
|
|
return;
|
|
}
|
|
|
|
if (unlockId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/unlock", unlockId];
|
|
NSInteger statusCode = 0;
|
|
NSDictionary* response = apiRequestWithStatusCLI(endpoint, @"POST", @{}, publicKey, secretKey, &statusCode);
|
|
|
|
if (statusCode == 428) {
|
|
if (handleSudoChallenge(response, @"POST", endpoint, @"{}", publicKey, secretKey)) {
|
|
printf("%sSnapshot unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
exit(1);
|
|
}
|
|
} else if (statusCode >= 200 && statusCode < 300) {
|
|
printf("%sSnapshot unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (cloneId) {
|
|
NSString* endpoint = [NSString stringWithFormat:@"/snapshots/%@/clone", cloneId];
|
|
NSMutableDictionary* payload = [NSMutableDictionary dictionary];
|
|
payload[@"clone_type"] = cloneType ?: @"session";
|
|
if (name) payload[@"name"] = name;
|
|
if (ports) {
|
|
NSArray* portStrings = [ports componentsSeparatedByString:@","];
|
|
NSMutableArray* portNumbers = [NSMutableArray array];
|
|
for (NSString* p in portStrings) {
|
|
[portNumbers addObject:@([p intValue])];
|
|
}
|
|
payload[@"ports"] = portNumbers;
|
|
}
|
|
|
|
NSDictionary* result = apiRequestCLI(endpoint, @"POST", payload, publicKey, secretKey);
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%sSnapshot cloned%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
return;
|
|
}
|
|
|
|
fprintf(stderr, "%sError: Use --list, --info, --session, --service, --restore, --delete, --lock, --unlock, or --clone%s\n",
|
|
[RED UTF8String], [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Logs Command
|
|
// ============================================================================
|
|
|
|
void cmdLogs(NSArray* args) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
|
|
NSString* source = @"all";
|
|
NSInteger lines = 100;
|
|
NSString* since = @"1h";
|
|
NSString* grepPattern = nil;
|
|
BOOL follow = NO;
|
|
|
|
for (NSUInteger i = 0; i < [args count]; i++) {
|
|
NSString* arg = args[i];
|
|
if (([arg isEqualToString:@"--source"] || [arg isEqualToString:@"-s"]) && i + 1 < [args count]) {
|
|
source = args[++i];
|
|
} else if (([arg isEqualToString:@"--lines"] || [arg isEqualToString:@"-n"]) && i + 1 < [args count]) {
|
|
lines = [args[++i] integerValue];
|
|
} else if ([arg isEqualToString:@"--since"] && i + 1 < [args count]) {
|
|
since = args[++i];
|
|
} else if (([arg isEqualToString:@"--grep"] || [arg isEqualToString:@"-g"]) && i + 1 < [args count]) {
|
|
grepPattern = args[++i];
|
|
} else if ([arg isEqualToString:@"--follow"] || [arg isEqualToString:@"-f"]) {
|
|
follow = YES;
|
|
}
|
|
}
|
|
|
|
if (follow) {
|
|
// Streaming logs via SSE
|
|
NSMutableString* endpoint = [NSMutableString stringWithFormat:@"/paas/logs/stream?source=%@", source];
|
|
if (grepPattern) {
|
|
NSString* encoded = [grepPattern stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
|
[endpoint appendFormat:@"&grep=%@", encoded];
|
|
}
|
|
|
|
NSString* urlStr = [NSString stringWithFormat:@"%@%@", UN_PORTAL_BASE, endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlStr];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
request.HTTPMethod = @"GET";
|
|
[request setValue:@"text/event-stream" forHTTPHeaderField:@"Accept"];
|
|
|
|
// Add HMAC authentication headers
|
|
if (secretKey && [secretKey length] > 0) {
|
|
NSString* timestamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
|
|
NSString* message = [NSString stringWithFormat:@"%@:GET:%@:", timestamp, endpoint];
|
|
NSString* signature = hmacSign(secretKey, message);
|
|
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
|
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
|
|
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
|
} else {
|
|
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
|
}
|
|
|
|
printf("Streaming logs (Ctrl+C to stop)...\n");
|
|
|
|
// Use synchronous download for SSE - will block
|
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
__block NSData* responseData = nil;
|
|
|
|
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
|
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
|
|
responseData = data;
|
|
dispatch_semaphore_signal(semaphore);
|
|
}];
|
|
[task resume];
|
|
|
|
// For SSE, we need to handle streaming differently - just show we attempted
|
|
// Real SSE would need a custom NSURLSession delegate
|
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
|
|
if (responseData) {
|
|
NSString* output = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
|
|
printf("%s\n", [output UTF8String]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fetch logs
|
|
NSMutableString* endpoint = [NSMutableString stringWithFormat:@"/paas/logs?source=%@&lines=%ld&since=%@",
|
|
source, (long)lines, since];
|
|
if (grepPattern) {
|
|
NSString* encoded = [grepPattern stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
|
[endpoint appendFormat:@"&grep=%@", encoded];
|
|
}
|
|
|
|
// Call Portal API for logs
|
|
NSString* urlStr = [NSString stringWithFormat:@"%@%@", UN_PORTAL_BASE, endpoint];
|
|
NSURL* url = [NSURL URLWithString:urlStr];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
request.HTTPMethod = @"GET";
|
|
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
|
|
|
// Add HMAC authentication headers
|
|
if (secretKey && [secretKey length] > 0) {
|
|
NSString* timestamp = [NSString stringWithFormat:@"%ld", (long)[[NSDate date] timeIntervalSince1970]];
|
|
NSString* message = [NSString stringWithFormat:@"%@:GET:%@:", timestamp, endpoint];
|
|
NSString* signature = hmacSign(secretKey, message);
|
|
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
|
[request setValue:timestamp forHTTPHeaderField:@"X-Timestamp"];
|
|
[request setValue:signature forHTTPHeaderField:@"X-Signature"];
|
|
} else {
|
|
[request setValue:[NSString stringWithFormat:@"Bearer %@", publicKey] forHTTPHeaderField:@"Authorization"];
|
|
}
|
|
|
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
__block NSDictionary* responseData = nil;
|
|
__block NSInteger statusCode = 0;
|
|
|
|
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
|
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
|
|
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
|
|
statusCode = httpResponse.statusCode;
|
|
if (data) {
|
|
responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
}
|
|
dispatch_semaphore_signal(semaphore);
|
|
}];
|
|
[task resume];
|
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
|
|
if (statusCode == 200 && responseData) {
|
|
NSArray* logLines = responseData[@"logs"];
|
|
if (logLines && [logLines isKindOfClass:[NSArray class]]) {
|
|
for (NSDictionary* entry in logLines) {
|
|
NSString* src = entry[@"source"] ?: @"unknown";
|
|
NSString* msg = entry[@"line"] ?: @"";
|
|
printf("[%s] %s\n", [src UTF8String], [msg UTF8String]);
|
|
}
|
|
} else {
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:responseData options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
}
|
|
} else {
|
|
fprintf(stderr, "%sError: HTTP %ld%s\n", [RED UTF8String], (long)statusCode, [RESET UTF8String]);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Health Command
|
|
// ============================================================================
|
|
|
|
void cmdHealth(void) {
|
|
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/health", UN_API_BASE]];
|
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
|
request.HTTPMethod = @"GET";
|
|
request.timeoutInterval = 10;
|
|
|
|
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
|
__block NSDictionary* responseData = nil;
|
|
__block NSInteger statusCode = 0;
|
|
|
|
NSURLSessionDataTask* task = [[NSURLSession sharedSession] dataTaskWithRequest:request
|
|
completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
|
|
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
|
|
statusCode = httpResponse.statusCode;
|
|
if (data) {
|
|
responseData = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
}
|
|
dispatch_semaphore_signal(semaphore);
|
|
}];
|
|
[task resume];
|
|
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
|
|
|
|
if (statusCode == 200) {
|
|
printf("%sAPI is healthy%s\n", [GREEN UTF8String], [RESET UTF8String]);
|
|
} else {
|
|
printf("%sAPI may be unhealthy%s\n", [RED UTF8String], [RESET UTF8String]);
|
|
}
|
|
|
|
if (responseData) {
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:responseData options:NSJSONWritingPrettyPrinted error:nil];
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Version Command
|
|
// ============================================================================
|
|
|
|
void cmdVersion(void) {
|
|
printf("un.m version 1.0.0\n");
|
|
printf("API: %s\n", [UN_API_BASE UTF8String]);
|
|
printf("Portal: %s\n", [UN_PORTAL_BASE UTF8String]);
|
|
}
|
|
|
|
void cmdLanguages(BOOL jsonOutput) {
|
|
NSString* publicKey, *secretKey;
|
|
UNGetApiKeysCLI(&publicKey, &secretKey);
|
|
|
|
NSDictionary* result = apiRequestCLI(@"/languages", @"GET", nil, publicKey, secretKey);
|
|
NSArray* languages = result[@"languages"];
|
|
|
|
if (jsonOutput) {
|
|
// Output as JSON array
|
|
NSError* error = nil;
|
|
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:languages options:0 error:&error];
|
|
if (!error && jsonData) {
|
|
NSString* jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
printf("%s\n", [jsonString UTF8String]);
|
|
}
|
|
} else {
|
|
// Output one language per line
|
|
for (NSString* lang in languages) {
|
|
printf("%s\n", [lang UTF8String]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void showHelp(void) {
|
|
printf("unsandbox - Execute code in secure sandboxes\n\n");
|
|
printf("Usage:\n");
|
|
printf(" un.m [options] <source_file>\n");
|
|
printf(" un.m session [options]\n");
|
|
printf(" un.m service [options]\n");
|
|
printf(" un.m image [options]\n");
|
|
printf(" un.m languages [--json]\n");
|
|
printf(" un.m key [options]\n\n");
|
|
printf("Execute options:\n");
|
|
printf(" -e KEY=VALUE Environment variable (multiple allowed)\n");
|
|
printf(" -f FILE Input file (multiple allowed)\n");
|
|
printf(" -a Return artifacts\n");
|
|
printf(" -o DIR Output directory for artifacts\n");
|
|
printf(" -n MODE Network mode (zerotrust|semitrusted)\n");
|
|
printf(" -v N vCPU count (1-8)\n\n");
|
|
printf("Session options:\n");
|
|
printf(" --list List active sessions\n");
|
|
printf(" --kill ID Terminate session\n");
|
|
printf(" --shell NAME Shell/REPL (default: bash)\n\n");
|
|
printf("Service options:\n");
|
|
printf(" --list List services\n");
|
|
printf(" --info ID Get service details\n");
|
|
printf(" --logs ID Get service logs\n");
|
|
printf(" --freeze ID Freeze service\n");
|
|
printf(" --unfreeze ID Unfreeze service\n");
|
|
printf(" --destroy ID Destroy service\n");
|
|
printf(" --name NAME Create service with name\n");
|
|
printf(" --ports PORTS Comma-separated ports\n");
|
|
printf(" --bootstrap CMD Bootstrap command\n\n");
|
|
printf("Image options:\n");
|
|
printf(" --list, -l List all images\n");
|
|
printf(" --info ID Get image details\n");
|
|
printf(" --delete ID Delete an image\n");
|
|
printf(" --lock ID Lock image to prevent deletion\n");
|
|
printf(" --unlock ID Unlock image\n");
|
|
printf(" --publish ID Publish image from service/snapshot\n");
|
|
printf(" --source-type TYPE Source type: service or snapshot\n");
|
|
printf(" --visibility ID MODE Set visibility: private, unlisted, or public\n");
|
|
printf(" --spawn ID Spawn new service from image\n");
|
|
printf(" --clone ID Clone an image\n");
|
|
printf(" --name NAME Name for spawned service or cloned image\n");
|
|
printf(" --ports PORTS Ports for spawned service\n\n");
|
|
printf("Languages options:\n");
|
|
printf(" --json Output as JSON array\n\n");
|
|
printf("Library Usage:\n");
|
|
printf(" #import \"un.m\"\n");
|
|
printf(" UNClient *client = [[UNClient alloc] init];\n");
|
|
printf(" NSDictionary *result = [client execute:@\"python\" code:@\"print('Hello')\"];\n");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Entry Point
|
|
// ============================================================================
|
|
|
|
#ifndef UN_LIBRARY_ONLY
|
|
|
|
int main(int argc, const char* argv[]) {
|
|
@autoreleasepool {
|
|
if (argc < 2) {
|
|
showHelp();
|
|
return 1;
|
|
}
|
|
|
|
NSMutableArray* args = [NSMutableArray array];
|
|
for (int i = 1; i < argc; i++) {
|
|
[args addObject:[NSString stringWithUTF8String:argv[i]]];
|
|
}
|
|
|
|
NSString* firstArg = args[0];
|
|
|
|
if ([firstArg isEqualToString:@"--help"] || [firstArg isEqualToString:@"-h"]) {
|
|
showHelp();
|
|
return 0;
|
|
}
|
|
|
|
if ([firstArg isEqualToString:@"session"]) {
|
|
cmdSession([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
|
} else if ([firstArg isEqualToString:@"service"]) {
|
|
cmdService([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
|
} else if ([firstArg isEqualToString:@"image"]) {
|
|
cmdImage([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
|
} else if ([firstArg isEqualToString:@"languages"]) {
|
|
BOOL jsonOutput = NO;
|
|
for (NSUInteger i = 1; i < [args count]; i++) {
|
|
if ([args[i] isEqualToString:@"--json"]) {
|
|
jsonOutput = YES;
|
|
}
|
|
}
|
|
cmdLanguages(jsonOutput);
|
|
} else if ([firstArg isEqualToString:@"key"]) {
|
|
cmdKey([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
|
} else if ([firstArg isEqualToString:@"snapshot"]) {
|
|
cmdSnapshot([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
|
} else if ([firstArg isEqualToString:@"logs"]) {
|
|
cmdLogs([args subarrayWithRange:NSMakeRange(1, [args count] - 1)]);
|
|
} else if ([firstArg isEqualToString:@"health"]) {
|
|
cmdHealth();
|
|
} else if ([firstArg isEqualToString:@"version"] || [firstArg isEqualToString:@"--version"]) {
|
|
cmdVersion();
|
|
} else {
|
|
cmdExecute(args);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif
|