|
| 1 | +/* |
| 2 | + * The framed server and request validation for the private Simulator AX reader. |
| 3 | + * The runtime binding is isolated in SnapshotBridgeRuntime.m. |
| 4 | + */ |
| 5 | + |
| 6 | +#import "SnapshotBridgeRuntime.h" |
| 7 | + |
| 8 | +#import <Foundation/Foundation.h> |
| 9 | + |
| 10 | +#import <arpa/inet.h> |
| 11 | +#import <errno.h> |
| 12 | +#import <limits.h> |
| 13 | +#import <math.h> |
| 14 | +#import <poll.h> |
| 15 | +#import <sys/socket.h> |
| 16 | +#import <sys/stat.h> |
| 17 | +#import <sys/types.h> |
| 18 | +#import <sys/un.h> |
| 19 | +#import <unistd.h> |
| 20 | + |
| 21 | +static const int kDefaultIdleTimeoutSeconds = 60; |
| 22 | + |
| 23 | +static void bridgeLog(NSString *message) |
| 24 | +{ |
| 25 | + fprintf(stderr, "[agent-device-snapshot-bridge] %s\n", message.UTF8String ?: "(no message)"); |
| 26 | + fflush(stderr); |
| 27 | +} |
| 28 | + |
| 29 | +NSDictionary *failureResponse(NSString *requestId, |
| 30 | + NSString *kind, |
| 31 | + NSString *code, |
| 32 | + NSString *message) |
| 33 | +{ |
| 34 | + return @{ |
| 35 | + kProtocolVersionKey : @(kProtocolVersion), |
| 36 | + kSourceVersionKey : kSourceVersion, |
| 37 | + kRequestIdKey : requestId ?: @"", |
| 38 | + @"ok" : @NO, |
| 39 | + @"error_kind" : kind ?: @"reader_unavailable", |
| 40 | + @"error_code" : code ?: @"unknown", |
| 41 | + @"error" : message ?: @"snapshot bridge request failed", |
| 42 | + }; |
| 43 | +} |
| 44 | + |
| 45 | +static BOOL validBoundInteger(id value, NSUInteger minimum, NSUInteger maximum, NSUInteger *output) |
| 46 | +{ |
| 47 | + if (![value isKindOfClass:NSNumber.class]) return NO; |
| 48 | + NSNumber *number = value; |
| 49 | + if (number.doubleValue != floor(number.doubleValue)) return NO; |
| 50 | + if (number.unsignedIntegerValue < minimum || number.unsignedIntegerValue > maximum) return NO; |
| 51 | + if (output) *output = number.unsignedIntegerValue; |
| 52 | + return YES; |
| 53 | +} |
| 54 | + |
| 55 | +static NSDictionary *handleRequest(NSDictionary *request) |
| 56 | +{ |
| 57 | + NSString *requestId = [request[kRequestIdKey] isKindOfClass:NSString.class] ? request[kRequestIdKey] : @""; |
| 58 | + id verb = request[@"verb"]; |
| 59 | + if (![verb isKindOfClass:NSString.class] || ![verb isEqualToString:@"describe"]) { |
| 60 | + return failureResponse(requestId, @"bad_request", @"verb-not-supported", @"snapshot bridge accepts describe requests only"); |
| 61 | + } |
| 62 | + NSNumber *pidValue = request[@"pid"]; |
| 63 | + if (!validBoundInteger(pidValue, 1, INT_MAX, NULL)) { |
| 64 | + return failureResponse(requestId, @"bad_request", @"pid-required", @"describe requires a positive target pid"); |
| 65 | + } |
| 66 | + NSString *generation = [request[@"generation"] isKindOfClass:NSString.class] ? request[@"generation"] : @""; |
| 67 | + if (generation.length == 0) { |
| 68 | + return failureResponse(requestId, @"bad_request", @"generation-required", @"describe requires an opaque target generation"); |
| 69 | + } |
| 70 | + id snapshotTree = request[@"snapshotTree"]; |
| 71 | + if (![snapshotTree isKindOfClass:NSNumber.class] || ![snapshotTree boolValue]) { |
| 72 | + return failureResponse(requestId, @"bad_request", @"snapshot-tree-required", @"snapshotTree must be enabled"); |
| 73 | + } |
| 74 | + id automationMode = request[@"automationMode"]; |
| 75 | + if (![automationMode isKindOfClass:NSNumber.class] || ![automationMode boolValue]) { |
| 76 | + return failureResponse(requestId, @"bad_request", @"automation-mode-required", @"automationMode must be enabled"); |
| 77 | + } |
| 78 | + NSUInteger maxDepth = 0; |
| 79 | + NSUInteger maxNodes = 0; |
| 80 | + NSUInteger maxDurationMs = 0; |
| 81 | + NSUInteger maxResponseBytes = 0; |
| 82 | + if (!validBoundInteger(request[@"maxDepth"], 0, kMaximumDepth, &maxDepth) || |
| 83 | + !validBoundInteger(request[@"maxNodes"], 1, kMaximumNodes, &maxNodes) || |
| 84 | + !validBoundInteger(request[@"maxDurationMs"], 1, kMaximumDurationMs, &maxDurationMs) || |
| 85 | + !validBoundInteger(request[@"maxResponseBytes"], 1024, kMaximumFrameBytes, &maxResponseBytes)) { |
| 86 | + return failureResponse(requestId, @"bad_request", @"bounds-invalid", @"snapshot bridge request bounds are outside the bridge limits"); |
| 87 | + } |
| 88 | + |
| 89 | + NSString *setupError = nil; |
| 90 | + BridgeRuntime *runtime = sharedRuntime(&setupError); |
| 91 | + if (!runtime) { |
| 92 | + NSMutableDictionary *unavailable = [failureResponse(requestId, @"unsupported", @"runtime-unavailable", setupError) mutableCopy]; |
| 93 | + unavailable[@"pid"] = pidValue; |
| 94 | + unavailable[@"generation"] = generation; |
| 95 | + return unavailable; |
| 96 | + } |
| 97 | + NSDictionary *error = nil; |
| 98 | + NSDictionary *response = [runtime snapshotForProcess:pidValue.intValue |
| 99 | + maxDepth:maxDepth |
| 100 | + maxNodes:maxNodes |
| 101 | + requestId:requestId |
| 102 | + generation:generation |
| 103 | + maxDurationMs:maxDurationMs |
| 104 | + error:&error]; |
| 105 | + if (response) return response; |
| 106 | + if (error) { |
| 107 | + NSMutableDictionary *annotated = [error mutableCopy]; |
| 108 | + annotated[@"pid"] = pidValue; |
| 109 | + annotated[@"generation"] = generation; |
| 110 | + return annotated; |
| 111 | + } |
| 112 | + return failureResponse(requestId, @"reader_unavailable", @"empty-response", @"AX bridge returned no response"); |
| 113 | +} |
| 114 | + |
| 115 | +static BOOL readFully(int fd, void *buffer, size_t length) |
| 116 | +{ |
| 117 | + size_t offset = 0; |
| 118 | + while (offset < length) { |
| 119 | + ssize_t count = recv(fd, (char *)buffer + offset, length - offset, 0); |
| 120 | + if (count > 0) { |
| 121 | + offset += (size_t)count; |
| 122 | + continue; |
| 123 | + } |
| 124 | + if (count < 0 && errno == EINTR) continue; |
| 125 | + return NO; |
| 126 | + } |
| 127 | + return YES; |
| 128 | +} |
| 129 | + |
| 130 | +static BOOL writeFully(int fd, const void *buffer, size_t length) |
| 131 | +{ |
| 132 | + size_t offset = 0; |
| 133 | + while (offset < length) { |
| 134 | + ssize_t count = send(fd, (const char *)buffer + offset, length - offset, MSG_NOSIGNAL); |
| 135 | + if (count > 0) { |
| 136 | + offset += (size_t)count; |
| 137 | + continue; |
| 138 | + } |
| 139 | + if (count < 0 && errno == EINTR) continue; |
| 140 | + return NO; |
| 141 | + } |
| 142 | + return YES; |
| 143 | +} |
| 144 | + |
| 145 | +static NSData *serializedResponse(NSDictionary *response, NSUInteger maxResponseBytes) |
| 146 | +{ |
| 147 | + NSError *error = nil; |
| 148 | + NSData *data = nil; |
| 149 | + @try { |
| 150 | + data = [NSJSONSerialization dataWithJSONObject:response options:0 error:&error]; |
| 151 | + if (data && data.length + sizeof(uint32_t) <= maxResponseBytes) return data; |
| 152 | + } @catch (NSException *exception) { |
| 153 | + bridgeLog(exception.reason ?: @"response serialization raised an exception"); |
| 154 | + } |
| 155 | + NSMutableDictionary *fallback = [failureResponse( |
| 156 | + response[kRequestIdKey], |
| 157 | + data ? @"response_limit_exceeded" : @"malformed_tree", |
| 158 | + data ? @"response-too-large" : @"response-not-json-safe", |
| 159 | + data ? @"snapshot response exceeds the per-request response bound" : (error.localizedDescription ?: @"response was not JSON serializable")) mutableCopy]; |
| 160 | + if (response[@"pid"] != nil) fallback[@"pid"] = response[@"pid"]; |
| 161 | + if (response[@"generation"] != nil) fallback[@"generation"] = response[@"generation"]; |
| 162 | + return [NSJSONSerialization dataWithJSONObject:fallback options:0 error:NULL]; |
| 163 | +} |
| 164 | + |
| 165 | +static NSUInteger responseLimitForRequest(id request) |
| 166 | +{ |
| 167 | + if (![request isKindOfClass:NSDictionary.class]) return kMaximumFrameBytes; |
| 168 | + NSNumber *value = request[@"maxResponseBytes"]; |
| 169 | + if (![value isKindOfClass:NSNumber.class]) return kMaximumFrameBytes; |
| 170 | + NSUInteger result = value.unsignedIntegerValue; |
| 171 | + return result >= 1024 && result <= kMaximumFrameBytes ? result : kMaximumFrameBytes; |
| 172 | +} |
| 173 | + |
| 174 | +static int serve(NSString *socketPath, int idleTimeoutSeconds, BOOL exitOnDisconnect) |
| 175 | +{ |
| 176 | + if (socketPath.length == 0 || socketPath.length >= sizeof(((struct sockaddr_un *)0)->sun_path)) { |
| 177 | + bridgeLog(@"socket path is empty or too long"); |
| 178 | + return 1; |
| 179 | + } |
| 180 | + |
| 181 | + int listener = socket(AF_UNIX, SOCK_STREAM, 0); |
| 182 | + if (listener < 0) { |
| 183 | + bridgeLog([NSString stringWithFormat:@"socket failed: %s", strerror(errno)]); |
| 184 | + return 1; |
| 185 | + } |
| 186 | + struct sockaddr_un address = {0}; |
| 187 | + address.sun_family = AF_UNIX; |
| 188 | + strlcpy(address.sun_path, socketPath.fileSystemRepresentation, sizeof(address.sun_path)); |
| 189 | + unlink(address.sun_path); |
| 190 | + if (bind(listener, (struct sockaddr *)&address, sizeof(address)) != 0 || listen(listener, 4) != 0) { |
| 191 | + bridgeLog([NSString stringWithFormat:@"bind/listen failed for %@: %s", socketPath, strerror(errno)]); |
| 192 | + close(listener); |
| 193 | + return 1; |
| 194 | + } |
| 195 | + chmod(address.sun_path, S_IRUSR | S_IWUSR); |
| 196 | + bridgeLog([NSString stringWithFormat:@"serving protocol %lu on %@", (unsigned long)kProtocolVersion, socketPath]); |
| 197 | + |
| 198 | + BOOL done = NO; |
| 199 | + while (!done) { |
| 200 | + struct pollfd waitForClient = {.fd = listener, .events = POLLIN, .revents = 0}; |
| 201 | + int ready = poll(&waitForClient, 1, idleTimeoutSeconds * 1000); |
| 202 | + if (ready == 0) break; |
| 203 | + if (ready < 0) { |
| 204 | + if (errno == EINTR) continue; |
| 205 | + break; |
| 206 | + } |
| 207 | + int connection = accept(listener, NULL, NULL); |
| 208 | + if (connection < 0) { |
| 209 | + if (errno == EINTR) continue; |
| 210 | + break; |
| 211 | + } |
| 212 | + struct timeval timeout = {.tv_sec = idleTimeoutSeconds, .tv_usec = 0}; |
| 213 | + setsockopt(connection, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); |
| 214 | + setsockopt(connection, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); |
| 215 | + while (YES) { |
| 216 | + @autoreleasepool { |
| 217 | + uint32_t networkLength = 0; |
| 218 | + if (!readFully(connection, &networkLength, sizeof(networkLength))) break; |
| 219 | + uint32_t length = ntohl(networkLength); |
| 220 | + if (length == 0 || length > kMaximumFrameBytes) break; |
| 221 | + NSMutableData *body = [NSMutableData dataWithLength:length]; |
| 222 | + if (!readFully(connection, body.mutableBytes, length)) break; |
| 223 | + id parsed = [NSJSONSerialization JSONObjectWithData:body options:0 error:NULL]; |
| 224 | + NSDictionary *response = [parsed isKindOfClass:NSDictionary.class] |
| 225 | + ? handleRequest(parsed) |
| 226 | + : failureResponse(@"", @"bad_request", @"json-object-required", @"request frame must be a JSON object"); |
| 227 | + NSData *encoded = serializedResponse(response, responseLimitForRequest(parsed)); |
| 228 | + if (encoded.length > kMaximumFrameBytes) break; |
| 229 | + uint32_t responseLength = htonl((uint32_t)encoded.length); |
| 230 | + if (!writeFully(connection, &responseLength, sizeof(responseLength)) || |
| 231 | + !writeFully(connection, encoded.bytes, encoded.length)) break; |
| 232 | + } |
| 233 | + } |
| 234 | + close(connection); |
| 235 | + if (exitOnDisconnect) done = YES; |
| 236 | + } |
| 237 | + |
| 238 | + close(listener); |
| 239 | + unlink(address.sun_path); |
| 240 | + return 0; |
| 241 | +} |
| 242 | + |
| 243 | +static int integerArgument(NSArray<NSString *> *arguments, NSString *flag, int fallback) |
| 244 | +{ |
| 245 | + for (NSUInteger index = 0; index + 1 < arguments.count; index += 1) { |
| 246 | + if (![arguments[index] isEqualToString:flag]) continue; |
| 247 | + NSInteger value = arguments[index + 1].integerValue; |
| 248 | + if (value > 0 && value <= INT_MAX) return (int)value; |
| 249 | + } |
| 250 | + return fallback; |
| 251 | +} |
| 252 | + |
| 253 | +static BOOL boolArgument(NSArray<NSString *> *arguments, NSString *flag, BOOL fallback) |
| 254 | +{ |
| 255 | + for (NSUInteger index = 0; index + 1 < arguments.count; index += 1) { |
| 256 | + if ([arguments[index] isEqualToString:flag]) return [arguments[index + 1] boolValue]; |
| 257 | + } |
| 258 | + return fallback; |
| 259 | +} |
| 260 | + |
| 261 | +int main(int argc, const char *argv[]) |
| 262 | +{ |
| 263 | + @autoreleasepool { |
| 264 | + if (argc < 3 || strcmp(argv[1], "serve") != 0) { |
| 265 | + fprintf(stderr, "Usage: %s serve <socket> [--idle-timeout <seconds>] [--exit-on-disconnect <bool>]\n", argv[0]); |
| 266 | + return 2; |
| 267 | + } |
| 268 | + NSMutableArray<NSString *> *arguments = [NSMutableArray array]; |
| 269 | + for (int index = 2; index < argc; index += 1) { |
| 270 | + NSString *value = [NSString stringWithUTF8String:argv[index]]; |
| 271 | + if (value) [arguments addObject:value]; |
| 272 | + } |
| 273 | + NSString *socketPath = arguments.firstObject; |
| 274 | + if (socketPath.length == 0) return 2; |
| 275 | + NSArray<NSString *> *flags = [arguments subarrayWithRange:NSMakeRange(1, arguments.count - 1)]; |
| 276 | + return serve(socketPath, |
| 277 | + integerArgument(flags, @"--idle-timeout", kDefaultIdleTimeoutSeconds), |
| 278 | + boolArgument(flags, @"--exit-on-disconnect", YES)); |
| 279 | + } |
| 280 | +} |
0 commit comments