feat: add HTTP 428 sudo OTP handling to all 42 SDK implementations
When destructive operations (service/snapshot/image destroy/unlock) receive HTTP 428 Precondition Required, the SDK now: 1. Extracts challenge_id from the JSON response 2. Prompts: 'Confirmation required. Check your email for a one-time code.' 3. Reads OTP from stdin 4. Retries with X-Sudo-OTP and X-Sudo-Challenge headers Languages updated: 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
This commit is contained in:
parent
1b06666f11
commit
a5155aed4f
42 changed files with 5163 additions and 321 deletions
|
|
@ -912,6 +912,132 @@ NSDictionary* UNLanguages(void) {
|
|||
// 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];
|
||||
|
|
@ -1435,8 +1561,21 @@ void cmdService(NSArray* args) {
|
|||
|
||||
if (destroyId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/services/%@", destroyId];
|
||||
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
|
||||
printf("%sService destroyed: %s%s\n", [GREEN UTF8String], [destroyId UTF8String], [RESET UTF8String]);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1585,8 +1724,21 @@ void cmdImage(NSArray* args) {
|
|||
|
||||
if (deleteId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/images/%@", deleteId];
|
||||
apiRequestCLI(endpoint, @"DELETE", nil, publicKey, secretKey);
|
||||
printf("%sImage deleted: %s%s\n", [GREEN UTF8String], [deleteId UTF8String], [RESET UTF8String]);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1599,8 +1751,21 @@ void cmdImage(NSArray* args) {
|
|||
|
||||
if (unlockId) {
|
||||
NSString* endpoint = [NSString stringWithFormat:@"/images/%@/unlock", unlockId];
|
||||
apiRequestCLI(endpoint, @"POST", nil, publicKey, secretKey);
|
||||
printf("%sImage unlocked: %s%s\n", [GREEN UTF8String], [unlockId UTF8String], [RESET UTF8String]);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue