diff --git a/Un.cs b/Un.cs index 0a7213d..5b42108 100644 --- a/Un.cs +++ b/Un.cs @@ -605,13 +605,31 @@ class Un catch (WebException ex) { string error = ""; + int statusCode = 0; if (ex.Response != null) { using (StreamReader reader = new StreamReader(ex.Response.GetResponseStream())) { error = reader.ReadToEnd(); } + if (ex.Response is HttpWebResponse httpResponse) + { + statusCode = (int)httpResponse.StatusCode; + } } + + // Check for clock drift errors + if (error.Contains("timestamp") && (statusCode == 401 || error.ToLower().Contains("expired") || error.ToLower().Contains("invalid"))) + { + Console.Error.WriteLine($"{RED}Error: Request timestamp expired (must be within 5 minutes of server time){RESET}"); + Console.Error.WriteLine($"{YELLOW}Your computer's clock may have drifted.{RESET}"); + Console.Error.WriteLine("Check your system time and sync with NTP if needed:"); + Console.Error.WriteLine(" Linux: sudo ntpdate -s time.nist.gov"); + Console.Error.WriteLine(" macOS: sudo sntp -sS time.apple.com"); + Console.Error.WriteLine(" Windows: w32tm /resync"); + Environment.Exit(1); + } + throw new Exception($"HTTP error - {error}"); } } diff --git a/Un.java b/Un.java index bccf8eb..64a1dd2 100644 --- a/Un.java +++ b/Un.java @@ -555,6 +555,18 @@ public class Un { int status = conn.getResponseCode(); if (status < 200 || status >= 300) { String error = readStream(conn.getErrorStream()); + + // Check for clock drift errors + if (error.contains("timestamp") && (status == 401 || error.toLowerCase().contains("expired") || error.toLowerCase().contains("invalid"))) { + System.err.println(RED + "Error: Request timestamp expired (must be within 5 minutes of server time)" + RESET); + System.err.println(YELLOW + "Your computer's clock may have drifted." + RESET); + System.err.println("Check your system time and sync with NTP if needed:"); + System.err.println(" Linux: sudo ntpdate -s time.nist.gov"); + System.err.println(" macOS: sudo sntp -sS time.apple.com"); + System.err.println(" Windows: w32tm /resync"); + System.exit(1); + } + throw new Exception("HTTP " + status + " - " + error); } diff --git a/un.clj b/un.clj index 75787a9..406b680 100644 --- a/un.clj +++ b/un.clj @@ -127,6 +127,22 @@ (let [message (str timestamp ":" method ":" path ":" body)] (hmac-sha256 secret-key message))) +(defn check-clock-drift-error [response] + (let [has-timestamp (or (str/includes? response "timestamp") + (str/includes? response "\"timestamp\"")) + has-401 (str/includes? response "401") + has-expired (str/includes? response "expired") + has-invalid (str/includes? response "invalid")] + (when (and has-timestamp (or has-401 has-expired has-invalid)) + (binding [*out* *err*] + (println (str red "Error: Request timestamp expired (must be within 5 minutes of server time)" reset)) + (println (str yellow "Your computer's clock may have drifted." reset)) + (println "Check your system time and sync with NTP if needed:") + (println " Linux: sudo ntpdate -s time.nist.gov") + (println " macOS: sudo sntp -sS time.apple.com") + (println " Windows: w32tm /resync")) + (System/exit 1)))) + (defn build-auth-headers [public-key secret-key method path body] (if secret-key (let [timestamp (str (quot (System/currentTimeMillis) 1000)) @@ -148,6 +164,7 @@ ["-d" (str "@" tmp-file)]) {:keys [out]} (apply sh args)] (io/delete-file tmp-file true) + (check-clock-drift-error out) out))) (defn curl-get [api-key endpoint] @@ -155,16 +172,20 @@ auth-headers (build-auth-headers public-key secret-key "GET" endpoint "") args (concat ["curl" "-s" (str "https://api.unsandbox.com" endpoint)] - auth-headers)] - (:out (apply sh args)))) + auth-headers) + result (:out (apply sh args))] + (check-clock-drift-error result) + result)) (defn curl-delete [api-key endpoint] (let [[public-key secret-key] (get-api-keys) auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "") args (concat ["curl" "-s" "-X" "DELETE" (str "https://api.unsandbox.com" endpoint)] - auth-headers)] - (:out (apply sh args)))) + auth-headers) + result (:out (apply sh args))] + (check-clock-drift-error result) + result)) (defn curl-portal-post [api-key endpoint json-data] (let [tmp-file (str "/tmp/un_clj_portal_" (rand-int 999999) ".json") @@ -178,6 +199,7 @@ ["-d" (str "@" tmp-file)]) {:keys [out]} (apply sh args)] (io/delete-file tmp-file true) + (check-clock-drift-error out) out))) (defn execute-command [file env-vars artifacts out-dir network vcpu] diff --git a/un.cob b/un.cob index 19ac291..0f32a47 100644 --- a/un.cob +++ b/un.cob @@ -293,14 +293,32 @@ "openssl dgst -sha256 -hmac '" FUNCTION TRIM(WS-SECRET-KEY) "' | cut -d' ' -f2); " - "curl -s -X POST https://api.unsandbox.com/execute " + "RESP=$(curl -s -w '\n%{http_code}' -X POST " + "https://api.unsandbox.com/execute " "-H 'Content-Type: application/json' " "-H 'Authorization: Bearer " FUNCTION TRIM(WS-PUBLIC-KEY) "' " "-H 'X-Timestamp: '$TS " "-H 'X-Signature: '$SIG " - "--data-binary \"$BODY\" -o /tmp/unsandbox_resp.json; " + "--data-binary \"$BODY\"); " + "HTTP_CODE=$(echo \"$RESP\" | tail -n1); " + "BODY=$(echo \"$RESP\" | sed '$d'); " + "echo \"$BODY\" > /tmp/unsandbox_resp.json; " + "if echo \"$BODY\" | grep -q '\"timestamp\"' && " + "(echo \"$HTTP_CODE\" | grep -q '401' || " + "echo \"$BODY\" | grep -qi 'expired' || " + "echo \"$BODY\" | grep -qi 'invalid'); then " + "echo -e '\x1b[31mError: Request timestamp expired " + "(must be within 5 minutes of server time)\x1b[0m' >&2; " + "echo -e '\x1b[33mYour computer'"'"'s clock may have " + "drifted.\x1b[0m' >&2; " + "echo 'Check your system time and sync with NTP if " + "needed:' >&2; " + "echo ' Linux: sudo ntpdate -s time.nist.gov' >&2; " + "echo ' macOS: sudo sntp -sS time.apple.com' >&2; " + "echo ' Windows: w32tm /resync' >&2; " + "rm -f /tmp/unsandbox_resp.json; exit 1; fi; " "jq -r '.stdout // empty' /tmp/unsandbox_resp.json | " "sed 's/^/\x1b[34m/' | sed 's/$/\x1b[0m/'; " "jq -r '.stderr // empty' /tmp/unsandbox_resp.json | " diff --git a/un.erl b/un.erl index 816a5b4..f8b7c5d 100755 --- a/un.erl +++ b/un.erl @@ -322,6 +322,25 @@ make_signature(SecretKey, Timestamp, Method, Path, Body) -> Message = Timestamp ++ ":" ++ Method ++ ":" ++ Path ++ ":" ++ Body, hmac_sha256(SecretKey, Message). +check_clock_drift_error(Response) -> + HasTimestamp = string:str(Response, "timestamp") > 0 orelse string:str(Response, "\"timestamp\"") > 0, + Has401 = string:str(Response, "401") > 0, + HasExpired = string:str(Response, "expired") > 0, + HasInvalid = string:str(Response, "invalid") > 0, + + case HasTimestamp andalso (Has401 orelse HasExpired orelse HasInvalid) of + true -> + io:format(standard_error, "\033[31mError: Request timestamp expired (must be within 5 minutes of server time)\033[0m~n", []), + io:format(standard_error, "\033[33mYour computer's clock may have drifted.\033[0m~n", []), + io:format(standard_error, "Check your system time and sync with NTP if needed:~n", []), + io:format(standard_error, " Linux: sudo ntpdate -s time.nist.gov~n", []), + io:format(standard_error, " macOS: sudo sntp -sS time.apple.com~n", []), + io:format(standard_error, " Windows: w32tm /resync~n", []), + halt(1); + false -> + ok + end. + build_auth_headers(PublicKey, SecretKey, Method, Path, Body) -> if SecretKey =/= false -> @@ -404,7 +423,9 @@ curl_post(ApiKey, Endpoint, TmpFile) -> " -H 'Content-Type: application/json'" ++ AuthHeaders ++ " -d @" ++ TmpFile, - os:cmd(Cmd). + Result = os:cmd(Cmd), + check_clock_drift_error(Result), + Result. curl_post_portal(ApiKey, Endpoint, Data) -> TmpFile = write_temp_file(Data), @@ -416,6 +437,7 @@ curl_post_portal(ApiKey, Endpoint, Data) -> " -d @" ++ TmpFile, Result = os:cmd(Cmd), file:delete(TmpFile), + check_clock_drift_error(Result), Result. curl_get(ApiKey, Endpoint) -> @@ -423,14 +445,18 @@ curl_get(ApiKey, Endpoint) -> AuthHeaders = build_auth_headers(PublicKey, SecretKey, "GET", Endpoint, ""), Cmd = "curl -s https://api.unsandbox.com" ++ Endpoint ++ AuthHeaders, - os:cmd(Cmd). + Result = os:cmd(Cmd), + check_clock_drift_error(Result), + Result. curl_delete(ApiKey, Endpoint) -> {PublicKey, SecretKey} = get_api_keys(), AuthHeaders = build_auth_headers(PublicKey, SecretKey, "DELETE", Endpoint, ""), Cmd = "curl -s -X DELETE https://api.unsandbox.com" ++ Endpoint ++ AuthHeaders, - os:cmd(Cmd). + Result = os:cmd(Cmd), + check_clock_drift_error(Result), + Result. %% Argument parsing parse_exec_args([], Opts) -> diff --git a/un.ex b/un.ex index eba8f7c..747c2ca 100755 --- a/un.ex +++ b/un.ex @@ -463,6 +463,7 @@ defmodule Un do {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true) File.rm(tmp_file) + check_clock_drift(output) output end @@ -497,6 +498,7 @@ defmodule Un do {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true) File.rm(tmp_file) + check_clock_drift(output) output end @@ -511,6 +513,7 @@ defmodule Un do {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true) + check_clock_drift(output) output end @@ -525,6 +528,7 @@ defmodule Un do {output, _exit} = System.cmd("curl", args, stderr_to_stdout: true) + check_clock_drift(output) output end @@ -554,6 +558,26 @@ defmodule Un do defp get_opt([_arg | rest], long, short, default) do get_opt(rest, long, short, default) end + + defp check_clock_drift(response) do + response_lower = String.downcase(response) + + # Check if response contains "timestamp" and error indicators + has_timestamp = String.contains?(response_lower, "timestamp") + has_error = String.contains?(response_lower, "401") or + String.contains?(response_lower, "expired") or + String.contains?(response_lower, "invalid") + + if has_timestamp and has_error do + IO.puts(:stderr, "#{@red}Error: Request timestamp expired (must be within 5 minutes of server time)#{@reset}") + IO.puts(:stderr, "#{@yellow}Your computer's clock may have drifted.") + IO.puts(:stderr, "Check your system time and sync with NTP if needed:") + IO.puts(:stderr, " Linux: sudo ntpdate -s time.nist.gov") + IO.puts(:stderr, " macOS: sudo sntp -sS time.apple.com") + IO.puts(:stderr, " Windows: w32tm /resync#{@reset}") + System.halt(1) + end + end end Un.main(System.argv()) diff --git a/un.f90 b/un.f90 index f649300..da8e43e 100644 --- a/un.f90 +++ b/un.f90 @@ -167,6 +167,16 @@ contains '-H "X-Timestamp: $TS" ', & '-H "X-Signature: $SIG" ', & '--data-binary "$BODY" -o /tmp/unsandbox_resp.json; ', & + 'RESP=$(cat /tmp/unsandbox_resp.json); ', & + 'if echo "$RESP" | grep -q "timestamp" && ', & + '(echo "$RESP" | grep -Eq "(401|expired|invalid)"); then ', & + 'echo -e "\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m" >&2; ', & + 'echo -e "\x1b[33mYour computer'\''s clock may have drifted.\x1b[0m" >&2; ', & + 'echo "Check your system time and sync with NTP if needed:" >&2; ', & + 'echo " Linux: sudo ntpdate -s time.nist.gov" >&2; ', & + 'echo " macOS: sudo sntp -sS time.apple.com" >&2; ', & + 'echo -e " Windows: w32tm /resync\x1b[0m" >&2; ', & + 'rm -f /tmp/unsandbox_resp.json; exit 1; fi; ', & 'jq -r ".stdout // empty" /tmp/unsandbox_resp.json | ', & 'sed "s/^/\x1b[34m/" | sed "s/$/\x1b[0m/"; ', & 'jq -r ".stderr // empty" /tmp/unsandbox_resp.json | ', & diff --git a/un.forth b/un.forth index 9f73713..2698f3b 100644 --- a/un.forth +++ b/un.forth @@ -157,7 +157,21 @@ s" TIMESTAMP=$(date +%s)" r@ write-line throw s" MESSAGE=\"$TIMESTAMP:POST:/execute:$BODY\"" r@ write-line throw s" SIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" -hex | sed 's/.*= //')" r@ write-line throw - s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_resp.json; jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'; jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2; rm -f /tmp/unsandbox_resp.json" r@ write-line throw + s" curl -s -X POST https://api.unsandbox.com/execute -H 'Content-Type: application/json' -H \"Authorization: Bearer $PUBLIC_KEY\" -H \"X-Timestamp: $TIMESTAMP\" -H \"X-Signature: $SIGNATURE\" -d \"$BODY\" -o /tmp/unsandbox_resp.json" r@ write-line throw + s" RESP=$(cat /tmp/unsandbox_resp.json)" r@ write-line throw + s" if echo \"$RESP\" | grep -q \"timestamp\" && (echo \"$RESP\" | grep -Eq \"(401|expired|invalid)\"); then" r@ write-line throw + s" echo -e '\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m' >&2" r@ write-line throw + s" echo -e '\\x1b[33mYour computer'\\''s clock may have drifted.\\x1b[0m' >&2" r@ write-line throw + s" echo 'Check your system time and sync with NTP if needed:' >&2" r@ write-line throw + s" echo ' Linux: sudo ntpdate -s time.nist.gov' >&2" r@ write-line throw + s" echo ' macOS: sudo sntp -sS time.apple.com' >&2" r@ write-line throw + s" echo -e ' Windows: w32tm /resync\\x1b[0m' >&2" r@ write-line throw + s" rm -f /tmp/unsandbox_resp.json" r@ write-line throw + s" exit 1" r@ write-line throw + s" fi" r@ write-line throw + s" jq -r '.stdout // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[34m/' | sed 's/$/\\x1b[0m/'" r@ write-line throw + s" jq -r '.stderr // empty' /tmp/unsandbox_resp.json | sed 's/^/\\x1b[31m/' | sed 's/$/\\x1b[0m/' >&2" r@ write-line throw + s" rm -f /tmp/unsandbox_resp.json" r@ write-line throw r> close-file throw s" chmod +x /tmp/unsandbox_script.sh && /tmp/unsandbox_script.sh && rm -f /tmp/unsandbox_script.sh" system diff --git a/un.fs b/un.fs index b0d094b..c188647 100644 --- a/un.fs +++ b/un.fs @@ -279,6 +279,17 @@ let apiRequest (endpoint: string) (method: string) (data: (string * obj) list op reader.ReadToEnd() else ex.Message + + // Check for clock drift error + if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then + eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset + eprintfn "%sYour computer's clock may have drifted.%s" yellow reset + eprintfn "Check your system time and sync with NTP if needed:" + eprintfn " Linux: sudo ntpdate -s time.nist.gov" + eprintfn " macOS: sudo sntp -sS time.apple.com" + eprintfn " Windows: w32tm /resync%s" reset + exit 1 + failwithf "HTTP error - %s" errorMsg let cmdExecute (args: Args) = @@ -432,6 +443,17 @@ let cmdKey (args: Args) = with _ -> body else ex.Message + + // Check for clock drift error + if errorMsg.Contains("timestamp") && (errorMsg.Contains("401") || errorMsg.Contains("expired") || errorMsg.Contains("invalid")) then + eprintfn "%sError: Request timestamp expired (must be within 5 minutes of server time)%s" red reset + eprintfn "%sYour computer's clock may have drifted.%s" yellow reset + eprintfn "Check your system time and sync with NTP if needed:" + eprintfn " Linux: sudo ntpdate -s time.nist.gov" + eprintfn " macOS: sudo sntp -sS time.apple.com" + eprintfn " Windows: w32tm /resync%s" reset + exit 1 + printfn "Reason: %s" errorMsg exit 1 diff --git a/un.hs b/un.hs index 3aa5127..dc15cf8 100644 --- a/un.hs +++ b/un.hs @@ -383,6 +383,28 @@ serviceCommand opts = do putStrLn $ green ++ "Service created" ++ reset putStrLn stdout +-- Check for clock drift error +checkClockDriftError :: String -> IO () +checkClockDriftError response = do + let hasTimestamp = "timestamp" `isPrefixOf` dropWhile (/= 't') response || + "\"timestamp\"" `isInfixOf` response + let has401 = "401" `isInfixOf` response + let hasExpired = "expired" `isInfixOf` response + let hasInvalid = "invalid" `isInfixOf` response + + when (hasTimestamp && (has401 || hasExpired || hasInvalid)) $ do + hPutStrLn stderr $ red ++ "Error: Request timestamp expired (must be within 5 minutes of server time)" ++ reset + hPutStrLn stderr $ yellow ++ "Your computer's clock may have drifted." ++ reset + hPutStrLn stderr "Check your system time and sync with NTP if needed:" + hPutStrLn stderr " Linux: sudo ntpdate -s time.nist.gov" + hPutStrLn stderr " macOS: sudo sntp -sS time.apple.com" + hPutStrLn stderr " Windows: w32tm /resync" + exitFailure + where + isInfixOf needle haystack = any (isPrefixOf needle) (tails haystack) + tails [] = [[]] + tails s@(_:xs) = s : tails xs + -- HTTP helpers using curl curlPost :: String -> String -> String -> IO (ExitCode, String, String) curlPost apiKey url body = do @@ -395,6 +417,8 @@ curlPost apiKey url body = do , url , "-H", "Content-Type: application/json" ] ++ authHeaders ++ ["-d", body]) "" + -- Check for clock drift error + checkClockDriftError stdout return (exitCode, stdout, stderr) curlGet :: String -> String -> IO (ExitCode, String, String) @@ -402,16 +426,22 @@ curlGet apiKey url = do (publicKey, secretKey) <- getApiKeys let path = drop (length "https://api.unsandbox.com") url authHeaders <- buildAuthHeaders publicKey secretKey "GET" path "" - readProcessWithExitCode "curl" + (exitCode, stdout, stderr) <- readProcessWithExitCode "curl" ([ "-s", url ] ++ authHeaders) "" + -- Check for clock drift error + checkClockDriftError stdout + return (exitCode, stdout, stderr) curlDelete :: String -> String -> IO (ExitCode, String, String) curlDelete apiKey url = do (publicKey, secretKey) <- getApiKeys let path = drop (length "https://api.unsandbox.com") url authHeaders <- buildAuthHeaders publicKey secretKey "DELETE" path "" - readProcessWithExitCode "curl" + (exitCode, stdout, stderr) <- readProcessWithExitCode "curl" ([ "-s", "-X", "DELETE", url ] ++ authHeaders) "" + -- Check for clock drift error + checkClockDriftError stdout + return (exitCode, stdout, stderr) -- Get API keys from environment getApiKeys :: IO (String, Maybe String) @@ -615,4 +645,6 @@ curlPostPortal apiKey url body = do , url , "-H", "Content-Type: application/json" ] ++ authHeaders ++ ["-d", body]) "" + -- Check for clock drift error + checkClockDriftError stdout return (exitCode, stdout, stderr) diff --git a/un.lisp b/un.lisp index c84a354..4eebbdb 100644 --- a/un.lisp +++ b/un.lisp @@ -101,40 +101,62 @@ while line do (format out "~a~%" line)) (uiop:wait-process process)))) +(defun check-clock-drift (response) + "Check if response indicates clock drift error" + (when (and (search "timestamp" response) + (or (search "401" response) + (search "expired" response) + (search "invalid" response))) + (format t "~aError: Request timestamp expired (must be within 5 minutes of server time)~a~%" *red* *reset*) + (format t "~aYour computer's clock may have drifted.~a~%" *yellow* *reset*) + (format t "Check your system time and sync with NTP if needed:~%") + (format t " Linux: sudo ntpdate -s time.nist.gov~%") + (format t " macOS: sudo sntp -sS time.apple.com~%") + (format t " Windows: w32tm /resync~a~%" *reset*) + (uiop:quit 1))) + (defun curl-post (api-key endpoint json-data) (let ((tmp-file (write-temp-file json-data))) (unwind-protect (destructuring-bind (public-key secret-key) (get-api-keys) - (let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)) - (base-args (list "curl" "-s" "-X" "POST" - (format nil "https://api.unsandbox.com~a" endpoint) - "-H" "Content-Type: application/json"))) - (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file)))))) + (let* ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)) + (base-args (list "curl" "-s" "-X" "POST" + (format nil "https://api.unsandbox.com~a" endpoint) + "-H" "Content-Type: application/json")) + (response (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file)))))) + (check-clock-drift response) + response)) (delete-file tmp-file)))) (defun curl-get (api-key endpoint) (destructuring-bind (public-key secret-key) (get-api-keys) - (let ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint "")) - (base-args (list "curl" "-s" - (format nil "https://api.unsandbox.com~a" endpoint)))) - (run-curl (append base-args auth-headers))))) + (let* ((auth-headers (build-auth-headers public-key secret-key "GET" endpoint "")) + (base-args (list "curl" "-s" + (format nil "https://api.unsandbox.com~a" endpoint))) + (response (run-curl (append base-args auth-headers)))) + (check-clock-drift response) + response))) (defun curl-delete (api-key endpoint) (destructuring-bind (public-key secret-key) (get-api-keys) - (let ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "")) - (base-args (list "curl" "-s" "-X" "DELETE" - (format nil "https://api.unsandbox.com~a" endpoint)))) - (run-curl (append base-args auth-headers))))) + (let* ((auth-headers (build-auth-headers public-key secret-key "DELETE" endpoint "")) + (base-args (list "curl" "-s" "-X" "DELETE" + (format nil "https://api.unsandbox.com~a" endpoint))) + (response (run-curl (append base-args auth-headers)))) + (check-clock-drift response) + response))) (defun curl-post-portal (api-key endpoint json-data) (let ((tmp-file (write-temp-file json-data))) (unwind-protect (destructuring-bind (public-key secret-key) (get-api-keys) - (let ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)) - (base-args (list "curl" "-s" "-X" "POST" - (format nil "~a~a" *portal-base* endpoint) - "-H" "Content-Type: application/json"))) - (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file)))))) + (let* ((auth-headers (build-auth-headers public-key secret-key "POST" endpoint json-data)) + (base-args (list "curl" "-s" "-X" "POST" + (format nil "~a~a" *portal-base* endpoint) + "-H" "Content-Type: application/json")) + (response (run-curl (append base-args auth-headers (list "-d" (format nil "@~a" tmp-file)))))) + (check-clock-drift response) + response)) (delete-file tmp-file)))) (defun get-api-keys () diff --git a/un.m b/un.m index d25b211..64edcc3 100644 --- a/un.m +++ b/un.m @@ -94,6 +94,24 @@ void getApiKeys(NSString** publicKey, NSString** secretKey) { } } +void checkClockDrift(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%s\n", [RESET UTF8String]); + exit(1); + } +} + NSString* hmacSha256Hex(NSString* key, NSString* message) { const char* cKey = [key UTF8String]; const char* cMessage = [message UTF8String]; @@ -170,6 +188,7 @@ NSDictionary* apiRequest(NSString* endpoint, NSString* method, NSDictionary* dat if (responseData) { NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; fprintf(stderr, "%s\n", [errMsg UTF8String]); + checkClockDrift(errMsg); } exit(1); } @@ -350,6 +369,7 @@ NSDictionary* portalRequest(NSString* endpoint, NSString* method, NSDictionary* if (responseData) { NSString* errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; fprintf(stderr, "%s\n", [errMsg UTF8String]); + checkClockDrift(errMsg); } exit(1); } diff --git a/un.ml b/un.ml index dbd04e2..1fa77da 100755 --- a/un.ml +++ b/un.ml @@ -112,6 +112,31 @@ let escape_json s = ) s; Buffer.contents buf +(* Check for clock drift errors *) +let check_clock_drift response = + let response_lower = String.lowercase_ascii response in + let contains_substring s sub = + try + let _ = Str.search_forward (Str.regexp_string sub) s 0 in + true + with Not_found -> false + in + let has_timestamp = contains_substring response_lower "timestamp" in + let has_401 = contains_substring response_lower "401" in + let has_expired = contains_substring response_lower "expired" in + let has_invalid = contains_substring response_lower "invalid" in + let has_error = has_401 || has_expired || has_invalid in + + if has_timestamp && has_error then begin + Printf.fprintf stderr "%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n" red reset; + Printf.fprintf stderr "%sYour computer's clock may have drifted.\n" yellow; + Printf.fprintf stderr "Check your system time and sync with NTP if needed:\n"; + Printf.fprintf stderr " Linux: sudo ntpdate -s time.nist.gov\n"; + Printf.fprintf stderr " macOS: sudo sntp -sS time.apple.com\n"; + Printf.fprintf stderr " Windows: w32tm /resync%s\n" reset; + exit 1 + end + (* Execute curl command *) let curl_post api_key endpoint json = let (public_key, secret_key) = get_api_keys () in @@ -130,6 +155,7 @@ let curl_post api_key endpoint json = let output = read_all "" in let _ = Unix.close_process_in ic in Sys.remove tmp_file; + check_clock_drift output; output let portal_curl_post api_key endpoint json = @@ -149,6 +175,7 @@ let portal_curl_post api_key endpoint json = let output = read_all "" in let _ = Unix.close_process_in ic in Sys.remove tmp_file; + check_clock_drift output; output let curl_get api_key endpoint = @@ -165,6 +192,7 @@ let curl_get api_key endpoint = in let output = read_all "" in let _ = Unix.close_process_in ic in + check_clock_drift output; output let curl_delete api_key endpoint = @@ -181,6 +209,7 @@ let curl_delete api_key endpoint = in let output = read_all "" in let _ = Unix.close_process_in ic in + check_clock_drift output; output (* Extract JSON value - simple regex-based parser *) diff --git a/un.pro b/un.pro index efaf07c..9de79d2 100644 --- a/un.pro +++ b/un.pro @@ -128,7 +128,7 @@ execute_file(Filename) :- % Build and execute curl command with HMAC format(atom(Cmd), - 'BODY=$(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_resp.json; jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json', + 'BODY=$(jq -Rs \'\'\''{language: "~w", code: .}\'\'\'\' < "~w"); TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/execute:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'\'\'s/.*= //\'\'\'\'); curl -s -X POST https://api.unsandbox.com/execute -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_resp.json; RESP=$(cat /tmp/unsandbox_resp.json); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; rm -f /tmp/unsandbox_resp.json; exit 1; fi; jq -r ".stdout // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[34m/" | sed "s/$/\\x1b[0m/"; jq -r ".stderr // empty" /tmp/unsandbox_resp.json | sed "s/^/\\x1b[31m/" | sed "s/$/\\x1b[0m/" >&2; rm -f /tmp/unsandbox_resp.json', [Language, Filename, SecretKey, PublicKey]), shell(Cmd, 0). @@ -137,7 +137,7 @@ session_list :- get_public_key(PublicKey), get_secret_key(SecretKey), format(atom(Cmd), - 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/sessions:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"', + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/sessions:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X GET https://api.unsandbox.com/sessions -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE"); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; exit 1; fi; echo "$RESP" | jq -r \'.sessions[] | "\\(.id) \\(.shell) \\(.status) \\(.created_at)"\' 2>/dev/null || echo "No active sessions"', [SecretKey, PublicKey]), shell(Cmd, 0). @@ -155,7 +155,7 @@ service_list :- get_public_key(PublicKey), get_secret_key(SecretKey), format(atom(Cmd), - 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"', + 'TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:GET:/services:"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X GET https://api.unsandbox.com/services -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE"); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; exit 1; fi; echo "$RESP" | jq -r \'.services[] | "\\(.id) \\(.name) \\(.status)"\' 2>/dev/null || echo "No services"', [SecretKey, PublicKey]), shell(Cmd, 0). @@ -252,11 +252,11 @@ validate_key(Extend) :- ( Extend = true -> % Build command for --extend mode format(atom(Cmd), - 'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null', + 'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); RESP=$(curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY"); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; exit 1; fi; PUBLIC_KEY=$(echo "$RESP" | jq -r ".public_key // \\"N/A\\""); xdg-open "~w/keys/extend?pk=$PUBLIC_KEY" 2>/dev/null', [SecretKey, PortalBase, PublicKey, PortalBase]) ; % Build command for normal validation format(atom(Cmd), - 'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json', + 'BODY=\'\'{}\'\'; TIMESTAMP=$(date +%s); MESSAGE="$TIMESTAMP:POST:/keys/validate:$BODY"; SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "~w" -hex | sed \'\'s/.*= //\'\'); curl -s -X POST ~w/keys/validate -H "Content-Type: application/json" -H "Authorization: Bearer ~w" -H "X-Timestamp: $TIMESTAMP" -H "X-Signature: $SIGNATURE" -d "$BODY" -o /tmp/unsandbox_key_resp.json; STATUS=$?; if [ $STATUS -ne 0 ]; then echo -e "\\x1b[31mInvalid\\x1b[0m"; exit 1; fi; RESP=$(cat /tmp/unsandbox_key_resp.json); if echo "$RESP" | grep -qi "timestamp" && echo "$RESP" | grep -Eqi "(401|expired|invalid)"; then echo -e "\\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\\x1b[0m" >&2; echo -e "\\x1b[33mYour computer'\''s clock may have drifted.\\x1b[0m" >&2; echo "Check your system time and sync with NTP if needed:" >&2; echo " Linux: sudo ntpdate -s time.nist.gov" >&2; echo " macOS: sudo sntp -sS time.apple.com" >&2; echo " Windows: w32tm /resync\\x1b[0m" >&2; rm -f /tmp/unsandbox_key_resp.json; exit 1; fi; EXPIRED=$(jq -r ".expired // false" /tmp/unsandbox_key_resp.json); if [ "$EXPIRED" = "true" ]; then echo -e "\\x1b[31mExpired\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expired: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo -e "\\x1b[33mTo renew: Visit https://unsandbox.com/keys/extend\\x1b[0m"; rm -f /tmp/unsandbox_key_resp.json; exit 1; else echo -e "\\x1b[32mValid\\x1b[0m"; echo "Public Key: $(jq -r ".public_key // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Tier: $(jq -r ".tier // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Status: $(jq -r ".status // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Expires: $(jq -r ".expires_at // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Time Remaining: $(jq -r ".time_remaining // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Rate Limit: $(jq -r ".rate_limit // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Burst: $(jq -r ".burst // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; echo "Concurrency: $(jq -r ".concurrency // \\"N/A\\"" /tmp/unsandbox_key_resp.json)"; fi; rm -f /tmp/unsandbox_key_resp.json', [SecretKey, PortalBase, PublicKey]) ), shell(Cmd, 0). diff --git a/un.r b/un.r index dd25ac8..e3a945f 100644 --- a/un.r +++ b/un.r @@ -93,6 +93,25 @@ get_api_keys <- function(args_key = NULL) { return(list(public_key = public_key, secret_key = secret_key)) } +check_clock_drift <- function(response_text) { + response_lower <- tolower(response_text) + has_timestamp <- grepl("timestamp", response_lower, fixed = TRUE) + has_401 <- grepl("401", response_lower, fixed = TRUE) + has_expired <- grepl("expired", response_lower, fixed = TRUE) + has_invalid <- grepl("invalid", response_lower, fixed = TRUE) + has_error <- has_401 || has_expired || has_invalid + + if (has_timestamp && has_error) { + cat(sprintf("%sError: Request timestamp expired (must be within 5 minutes of server time)%s\n", RED, RESET), file = stderr()) + cat(sprintf("%sYour computer's clock may have drifted.\n", YELLOW), file = stderr()) + cat("Check your system time and sync with NTP if needed:\n", file = stderr()) + cat(" Linux: sudo ntpdate -s time.nist.gov\n", file = stderr()) + cat(" macOS: sudo sntp -sS time.apple.com\n", file = stderr()) + cat(sprintf(" Windows: w32tm /resync%s\n", RESET), file = stderr()) + quit(status = 1) + } +} + api_request <- function(endpoint, public_key, secret_key, method = "GET", data = NULL) { url <- paste0(API_BASE, endpoint) headers <- add_headers( @@ -129,7 +148,9 @@ api_request <- function(endpoint, public_key, secret_key, method = "GET", data = stop(paste("Unsupported method:", method)) } - result <- fromJSON(content(response, "text", encoding = "UTF-8")) + response_text <- content(response, "text", encoding = "UTF-8") + check_clock_drift(response_text) + result <- fromJSON(response_text) return(result) }, error = function(e) { cat(sprintf("%sError: Request failed: %s%s\n", RED, e$message, RESET), file = stderr()) @@ -292,7 +313,9 @@ cmd_key <- function(args) { tryCatch({ response <- POST(url, headers, encode = "json", timeout(10)) - result <- fromJSON(content(response, "text", encoding = "UTF-8")) + response_text <- content(response, "text", encoding = "UTF-8") + check_clock_drift(response_text) + result <- fromJSON(response_text) if (!is.null(result$public_key)) { extend_url <- paste0(PORTAL_BASE, "/keys/extend?pk=", result$public_key) @@ -331,7 +354,9 @@ cmd_key <- function(args) { tryCatch({ response <- POST(url, headers, encode = "json", timeout(10)) - result <- fromJSON(content(response, "text", encoding = "UTF-8")) + response_text <- content(response, "text", encoding = "UTF-8") + check_clock_drift(response_text) + result <- fromJSON(response_text) status <- if (!is.null(result$status)) result$status else "Unknown" diff --git a/un.raku b/un.raku index 1d93c9a..b50f80b 100644 --- a/un.raku +++ b/un.raku @@ -148,6 +148,17 @@ sub api-request(Str $endpoint, Str $method, %data?, Str :$public-key!, Str :$sec exit 1; } + # Check for clock drift errors + if $body.contains('timestamp') && ($body.contains('401') || $body.contains('expired') || $body.contains('invalid')) { + note "{$RED}Error: Request timestamp expired (must be within 5 minutes of server time){$RESET}"; + note "{$YELLOW}Your computer's clock may have drifted.{$RESET}"; + note "{$YELLOW}Check your system time and sync with NTP if needed:{$RESET}"; + note "{$YELLOW} Linux: sudo ntpdate -s time.nist.gov{$RESET}"; + note "{$YELLOW} macOS: sudo sntp -sS time.apple.com{$RESET}"; + note "{$YELLOW} Windows: w32tm /resync{$RESET}"; + exit 1; + } + return from-json($body); } diff --git a/un.scm b/un.scm index cf0f3f0..39ab8cd 100644 --- a/un.scm +++ b/un.scm @@ -117,6 +117,18 @@ (loop (cons line lines))))))) (close-pipe port) (delete-file tmp-file) + ;; Check for clock drift errors + (when (and (string-contains output "timestamp") + (or (string-contains output "401") + (string-contains output "expired") + (string-contains output "invalid"))) + (format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset) + (format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset) + (format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset) + (format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset) + (format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset) + (format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset) + (exit 1)) output)) (define (curl-get api-key endpoint) @@ -133,6 +145,18 @@ (string-join (reverse lines) "\n") (loop (cons line lines))))))) (close-pipe port) + ;; Check for clock drift errors + (when (and (string-contains output "timestamp") + (or (string-contains output "401") + (string-contains output "expired") + (string-contains output "invalid"))) + (format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset) + (format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset) + (format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset) + (format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset) + (format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset) + (format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset) + (exit 1)) output)) (define (curl-delete api-key endpoint) @@ -149,6 +173,18 @@ (string-join (reverse lines) "\n") (loop (cons line lines))))))) (close-pipe port) + ;; Check for clock drift errors + (when (and (string-contains output "timestamp") + (or (string-contains output "401") + (string-contains output "expired") + (string-contains output "invalid"))) + (format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset) + (format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset) + (format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset) + (format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset) + (format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset) + (format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset) + (exit 1)) output)) (define (curl-post-portal api-key endpoint json-data) @@ -169,6 +205,18 @@ (loop (cons line lines))))))) (close-pipe port) (delete-file tmp-file) + ;; Check for clock drift errors + (when (and (string-contains output "timestamp") + (or (string-contains output "401") + (string-contains output "expired") + (string-contains output "invalid"))) + (format (current-error-port) "~aError: Request timestamp expired (must be within 5 minutes of server time)~a\n" red reset) + (format (current-error-port) "~aYour computer's clock may have drifted.~a\n" yellow reset) + (format (current-error-port) "~aCheck your system time and sync with NTP if needed:~a\n" yellow reset) + (format (current-error-port) "~a Linux: sudo ntpdate -s time.nist.gov~a\n" yellow reset) + (format (current-error-port) "~a macOS: sudo sntp -sS time.apple.com~a\n" yellow reset) + (format (current-error-port) "~a Windows: w32tm /resync~a\n" yellow reset) + (exit 1)) output)) (define (get-api-keys) diff --git a/un.zig b/un.zig index 56eafa9..e336063 100644 --- a/un.zig +++ b/un.zig @@ -362,6 +362,21 @@ pub fn main() !u8 { defer allocator.free(json_content); std.fs.cwd().deleteFile(json_file) catch {}; + // Check for clock drift errors + if (mem.indexOf(u8, json_content, "timestamp") != null and + (mem.indexOf(u8, json_content, "401") != null or + mem.indexOf(u8, json_content, "expired") != null or + mem.indexOf(u8, json_content, "invalid") != null)) + { + std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{}); + std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{}); + std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{}); + std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{}); + std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{}); + std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{}); + return 1; + } + // Simple JSON parsing to find public_key (looking for "public_key":"value") const pk_prefix = "\"public_key\":\""; var public_key_value: ?[]const u8 = null; @@ -401,6 +416,21 @@ pub fn main() !u8 { defer allocator.free(json_content); std.fs.cwd().deleteFile(json_file) catch {}; + // Check for clock drift errors + if (mem.indexOf(u8, json_content, "timestamp") != null and + (mem.indexOf(u8, json_content, "401") != null or + mem.indexOf(u8, json_content, "expired") != null or + mem.indexOf(u8, json_content, "invalid") != null)) + { + std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{}); + std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{}); + std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{}); + std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{}); + std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{}); + std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{}); + return 1; + } + // Simple JSON parsing (looking for specific fields) const status_prefix = "\"status\":\""; var status: ?[]const u8 = null; @@ -535,14 +565,44 @@ pub fn main() !u8 { // Execute with curl const auth_headers = try buildAuthCmd(allocator, "POST", "/execute", json_content, public_key, secret_key); defer allocator.free(auth_headers); - const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s}", .{ API_BASE, auth_headers, json_file }); + const response_file = "/tmp/unsandbox_response.json"; + const cmd = try std.fmt.allocPrint(allocator, "curl -s -X POST '{s}/execute' -H 'Content-Type: application/json' {s} -d @{s} -o {s}", .{ API_BASE, auth_headers, json_file, response_file }); defer allocator.free(cmd); - const result = std.c.system(cmd.ptr); - std.debug.print("\n", .{}); + _ = std.c.system(cmd.ptr); + + // Read response to check for clock drift errors + const response_content = fs.cwd().readFileAlloc(allocator, response_file, 10 * 1024 * 1024) catch |err| { + std.debug.print("\x1b[31mError reading response: {}\x1b[0m\n", .{err}); + std.fs.cwd().deleteFile(json_file) catch {}; + std.fs.cwd().deleteFile(response_file) catch {}; + return 1; + }; + defer allocator.free(response_content); + + // Check for clock drift errors + if (mem.indexOf(u8, response_content, "timestamp") != null and + (mem.indexOf(u8, response_content, "401") != null or + mem.indexOf(u8, response_content, "expired") != null or + mem.indexOf(u8, response_content, "invalid") != null)) + { + std.debug.print("\x1b[31mError: Request timestamp expired (must be within 5 minutes of server time)\x1b[0m\n", .{}); + std.debug.print("\x1b[33mYour computer's clock may have drifted.\x1b[0m\n", .{}); + std.debug.print("\x1b[33mCheck your system time and sync with NTP if needed:\x1b[0m\n", .{}); + std.debug.print("\x1b[33m Linux: sudo ntpdate -s time.nist.gov\x1b[0m\n", .{}); + std.debug.print("\x1b[33m macOS: sudo sntp -sS time.apple.com\x1b[0m\n", .{}); + std.debug.print("\x1b[33m Windows: w32tm /resync\x1b[0m\n", .{}); + std.fs.cwd().deleteFile(json_file) catch {}; + std.fs.cwd().deleteFile(response_file) catch {}; + return 1; + } + + // Print response + std.debug.print("{s}\n", .{response_content}); // Cleanup std.fs.cwd().deleteFile(json_file) catch {}; + std.fs.cwd().deleteFile(response_file) catch {}; - return if (result == 0) 0 else 1; + return 0; }