1//
2// TORController.m
3// Tor
4//
5// Created by Conrad Kramer on 5/10/14.
6//
7
8#import "TORController.h"
9
10#import <sys/socket.h>
11#import <sys/un.h>
12#import <netinet/in.h>
13#import <arpa/inet.h>
14
15#import "TORControlReplyCode.h"
16#import "TORControlCommand.h"
17#import "NSCharacterSet+PredefinedSets.h"
18
19NS_ASSUME_NONNULL_BEGIN
20
21#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000
22NSErrorDomain const TORControllerErrorDomain = @"TORControllerErrorDomain";
23#else
24NSString * const TORControllerErrorDomain = @"TORControllerErrorDomain";
25#endif
26
27static NSString * const TORControllerMidReplyLineSeparator = @"-";
28static NSString * const TORControllerDataReplyLineSeparator = @"+";
29static NSString * const TORControllerEndReplyLineSeparator = @" ";
30
31@implementation TORController {
32 NSURL *_url;
33 NSString *_host;
34 in_port_t _port;
35 dispatch_io_t _channel;
36 NSMutableArray<TORObserverBlock> *_blocks;
37 int sock;
38}
39
40+ (dispatch_queue_t)controlQueue {
41 static dispatch_queue_t controlQueue = NULL;
42 static dispatch_once_t onceToken;
43 dispatch_once(&onceToken, ^{
44 controlQueue = dispatch_queue_create("org.torproject.ios.control", DISPATCH_QUEUE_SERIAL);
45 });
46 return controlQueue;
47}
48
49- (instancetype)initWithSocketURL:(NSURL *)url {
50 NSParameterAssert(url.fileURL);
51 self = [super init];
52 if (!self)
53 return nil;
54
55 _url = [url copy];
56 _blocks = [NSMutableArray new];
57
58 [self connect:nil];
59
60 return self;
61}
62
63- (instancetype)initWithSocketHost:(NSString *)host port:(in_port_t)port {
64 NSParameterAssert(host && port);
65 self = [super init];
66 if (!self)
67 return nil;
68
69 _host = [host copy];
70 _port = port;
71 _blocks = [NSMutableArray new];
72
73 [self connect:nil];
74
75 return self;
76}
77
78- (instancetype)initWithControlPortFile:(NSURL *)file {
79 NSParameterAssert(file.fileURL);
80
81 // Expected example content:
82 // PORT=127.0.0.1:49651
83
84 NSError *error;
85 NSString *content = [[NSString alloc]
86 initWithContentsOfURL:file
87 encoding:NSUTF8StringEncoding error:&error];
88
89 NSAssert(!error, error.localizedDescription);
90
91 NSArray<NSString *> *address = [[[content componentsSeparatedByString:@"="]
92 .lastObject
93 stringByTrimmingCharactersInSet:
94 NSCharacterSet.whitespaceAndNewlineCharacterSet]
95 componentsSeparatedByString:@":"];
96
97 NSString *host = address.firstObject;
98 NSAssert(host, @"Provided file doesn't seem to be a valid control port file as written by Tor!");
99
100 NSInteger port = address.lastObject.integerValue;
101
102 return [self initWithSocketHost:host port:(in_port_t) port];
103}
104
105- (void)dealloc {
106 if (_channel)
107 dispatch_io_close(_channel, DISPATCH_IO_STOP);
108}
109
110#pragma mark - Connecting
111
112- (BOOL)isConnected {
113 return (_channel != nil);
114}
115
116- (BOOL)connect:(out NSError **)error {
117 if (_channel)
118 {
119 return NO;
120 }
121
122 self->sock = -1;
123
124 _events = [NSOrderedSet new];
125
126 if (_url)
127 {
128 struct sockaddr_un control_addr = {};
129 control_addr.sun_family = AF_UNIX;
130 strncpy(control_addr.sun_path, _url.fileSystemRepresentation, sizeof(control_addr.sun_path) - 1);
131 control_addr.sun_len = (unsigned char)SUN_LEN(&control_addr);
132
133 self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
134
135 if (connect(self->sock, (struct sockaddr *)&control_addr, control_addr.sun_len) == -1)
136 {
137 if (error)
138 {
139 *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
140 }
141
142 return NO;
143 }
144 }
145 else if (_host && _port) {
146 struct in_addr addr;
147
148 if (inet_aton(_host.UTF8String, &addr) == 0)
149 {
150 if (error)
151 {
152 *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
153 }
154
155 return NO;
156 }
157
158 struct sockaddr_in control_addr = {};
159 control_addr.sin_family = AF_INET;
160 control_addr.sin_port = htons(_port);
161 control_addr.sin_addr = addr;
162 control_addr.sin_len = (__uint8_t)sizeof(control_addr);
163
164 self->sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
165
166 if (connect(self->sock, (struct sockaddr *)&control_addr, control_addr.sin_len) == -1)
167 {
168 if (error)
169 {
170 *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
171 }
172
173 return NO;
174 }
175 }
176 else {
177 return NO;
178 }
179
180 __weak TORController *weakSelf = self;
181
182 _channel = dispatch_io_create(DISPATCH_IO_STREAM, self->sock, [self.class controlQueue], ^(int __unused error) {
183 close(self->sock);
184
185 TORController *strongSelf = weakSelf;
186 if (strongSelf)
187 {
188 strongSelf->_channel = nil;
189 }
190 });
191
192 if (!_channel)
193 {
194 return NO;
195 }
196
197 NSData *separator = [NSData dataWithBytes:"\x0d\x0a" length:2]; // also known as CR-LF or "\r\n"
198 NSData *period = [NSData dataWithBytes:"." length:1];
199
200 NSSet<NSString *> *lineSeparators = [NSSet setWithObjects:TORControllerMidReplyLineSeparator,
201 TORControllerDataReplyLineSeparator,
202 TORControllerEndReplyLineSeparator, nil];
203
204 __block NSMutableData *buffer = [NSMutableData new];
205 __block NSMutableArray<NSNumber *> *codes = [NSMutableArray new];
206 __block NSMutableArray<NSData *> *lines = [NSMutableArray new];
207 __block BOOL dataBlock = NO;
208
209 dispatch_io_set_low_water(_channel, 1);
210 dispatch_io_read(_channel, 0, SIZE_MAX, [self.class controlQueue], ^(bool __unused done, dispatch_data_t data, int __unused error) {
211 [buffer appendData:(NSData *)data];
212
213 NSRange separatorRange;
214 NSRange remainingRange = NSMakeRange(0, buffer.length);
215
216 while ((separatorRange = [buffer rangeOfData:separator options:0 range:remainingRange]).location != NSNotFound)
217 {
218 NSUInteger lineLength = separatorRange.location - remainingRange.location;
219 NSRange lineRange = NSMakeRange(remainingRange.location, lineLength);
220 remainingRange = NSMakeRange(remainingRange.location + lineLength + separator.length,
221 remainingRange.length - lineLength - separator.length);
222
223 if (dataBlock)
224 {
225 NSData *lineData = [buffer subdataWithRange:lineRange];
226
227 if ([lineData isEqualToData:period])
228 {
229 dataBlock = NO;
230 }
231 else {
232 if (lines.count > 0)
233 {
234 if (lines.lastObject.length > 0)
235 {
236 NSMutableData *lastData = lines.lastObject.mutableCopy;
237
238 // BUGFIX: Add in separator again. It is needed to pick apart multi-line results later!
239 [lastData appendData:separator];
240 [lastData appendData:lineData];
241
242 [lines replaceObjectAtIndex:(lines.count - 1) withObject:lastData];
243 }
244 else {
245 [lines replaceObjectAtIndex:(lines.count - 1) withObject:lineData];
246 }
247 }
248 else {
249 [lines addObject:lineData];
250 }
251 }
252
253 continue;
254 }
255
256 if (lineRange.length < 4)
257 {
258 continue;
259 }
260
261 NSString *statusCodeString = [[NSString alloc] initWithData:[buffer subdataWithRange:NSMakeRange(lineRange.location, 3)]
262 encoding:NSUTF8StringEncoding];
263
264 if ([statusCodeString rangeOfCharacterFromSet:NSCharacterSet.decimalDigitCharacterSet.invertedSet].location != NSNotFound)
265 {
266 continue;
267 }
268
269 NSString *lineTypeString = [[NSString alloc] initWithData:[buffer subdataWithRange:NSMakeRange(lineRange.location + 3, 1)]
270 encoding:NSUTF8StringEncoding];
271
272 if (![lineSeparators containsObject:lineTypeString])
273 {
274 continue;
275 }
276
277 [codes addObject:@(statusCodeString.integerValue)];
278 [lines addObject:[buffer subdataWithRange:NSMakeRange(lineRange.location + 4, lineRange.length - 4)]];
279
280 [buffer replaceBytesInRange:NSMakeRange(0, remainingRange.location) withBytes:NULL length:0];
281 remainingRange.location = 0;
282
283 if ([lineTypeString isEqualToString:TORControllerDataReplyLineSeparator])
284 {
285 dataBlock = YES;
286 }
287
288 if ([lineTypeString isEqualToString:TORControllerEndReplyLineSeparator])
289 {
290 NSArray<NSNumber *> *commandCodes = codes;
291 NSArray<NSData *> *commandLines = lines;
292 codes = [NSMutableArray new];
293 lines = [NSMutableArray new];
294
295 TORController *strongSelf = weakSelf;
296 if (!strongSelf)
297 {
298 continue;
299 }
300
301 for (TORObserverBlock observer in [strongSelf->_blocks copy]) {
302 BOOL stop = NO;
303 BOOL handled = observer(commandCodes, commandLines, &stop);
304
305 if (stop)
306 {
307 [strongSelf->_blocks removeObject:observer];
308 }
309
310 if (handled)
311 {
312 break;
313 }
314 }
315 }
316 }
317 });
318
319 return YES;
320}
321
322- (void)disconnect {
323 [self sendCommand:TORCommandSignalShutdown arguments:nil data:nil observer:^BOOL(NSArray<NSNumber *> * __unused codes, NSArray<NSData *> * __unused lines, BOOL * __unused stop) {
324 shutdown(self->sock, SHUT_RDWR);
325 self->_channel = nil;
326
327 return YES;
328 }];
329}
330
331#pragma mark - Receiving Responses
332
333- (id)addObserverForCircuitEstablished:(void (^)(BOOL established))block {
334 NSParameterAssert(block);
335
336 NSString *event = @"STATUS_CLIENT";
337 id observer = [self addObserverForStatusEvents:^(NSString *type, NSString * __unused severity, NSString *action, NSDictionary<NSString *, NSString *> * __unused arguments) {
338 if ([type isEqualToString:event]) {
339 if ([action isEqualToString:@"CIRCUIT_ESTABLISHED"]) {
340 block(YES);
341 return YES;
342 } else if ([action isEqualToString:@"CIRCUIT_NOT_ESTABLISHED"]) {
343 block(NO);
344 return YES;
345 }
346 }
347
348 return NO;
349 }];
350
351 void (^completion)(BOOL, NSError *) = ^(BOOL success, NSError * __unused error) {
352 if (!success)
353 [self removeObserver:observer];
354
355 [self getInfoForKeys:@[@"status/circuit-established"] completion:^(NSArray<NSString *> *values) {
356 if (values.count != 1)
357 return [self removeObserver:observer];
358
359 if (block)
360 block([values[0] boolValue]);
361 }];
362 };
363
364 dispatch_async([self.class controlQueue], ^{
365 if ([self->_events containsObject:event]) {
366 completion(YES, nil);
367 } else {
368 NSMutableOrderedSet *events = [self->_events mutableCopy];
369 [events addObject:event];
370 [self listenForEvents:events.array completion:completion];
371 }
372 });
373
374 return observer;
375}
376
377- (id)addObserverForStatusEvents:(BOOL (^)(NSString *type, NSString *severity, NSString *action, NSDictionary<NSString *, NSString *> *arguments))block {
378 NSParameterAssert(block);
379 return [self addObserver:^(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL * __unused stop) {
380 if (codes.firstObject.integerValue != 650)
381 return NO;
382
383 NSString *replyString = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
384 if (![replyString hasPrefix:@"STATUS_"])
385 return NO;
386
387 NSArray<NSString *> *components = [TORController parseStatus:replyString];
388 if (components.count < 3)
389 return NO;
390
391 NSMutableDictionary<NSString *, NSString *> *arguments = nil;
392 if (components.count > 3) {
393 arguments = [NSMutableDictionary new];
394 for (NSString *argument in [components subarrayWithRange:NSMakeRange(3, components.count - 3)]) {
395 NSArray<NSString *> *keyValuePair = [argument componentsSeparatedByString:@"="];
396 if (keyValuePair.count == 2) {
397 NSString *value = [keyValuePair[1] stringByReplacingOccurrencesOfString:@"\"" withString:@""];
398
399 [arguments setObject:value forKey:keyValuePair[0]];
400 }
401 }
402 }
403
404 NSString *type = (NSString * _Nonnull)components.firstObject;
405 return block(type, components[1], components[2], arguments);
406 }];
407}
408
409- (id)addObserver:(TORObserverBlock)observer {
410 NSParameterAssert(observer);
411 dispatch_async([self.class controlQueue], ^{
412 [self->_blocks addObject:observer];
413 });
414 return observer;
415}
416
417- (void)removeObserver:(nullable id)observer {
418 if (!observer)
419 return;
420
421 dispatch_async([self.class controlQueue], ^{
422 [self->_blocks removeObject:(id _Nonnull)observer];
423 });
424}
425
426#pragma mark - Sending Commands
427
428- (void)authenticateWithData:(NSData *)data completion:(void (^__nullable)(BOOL success, NSError * __nullable error))completion {
429 NSMutableString *hexString = [NSMutableString new];
430 for (NSUInteger idx = 0; idx < data.length; idx++)
431 [hexString appendFormat:@"%02x", ((const unsigned char *)data.bytes)[idx]];
432
433 [self sendCommand:TORCommandAuthenticate arguments:(hexString.length ? @[hexString] : nil) data:nil observer:^BOOL(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL *stop) {
434 NSUInteger code = codes.firstObject.unsignedIntegerValue;
435 if (code != TORControlReplyCodeOK && code != TORControlReplyCodeBadAuthentication)
436 return NO;
437
438 NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
439 NSDictionary<NSString *, NSString *> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil];
440 BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);
441 if (completion)
442 completion(success, success ? nil : [NSError errorWithDomain:TORControllerErrorDomain code:code userInfo:userInfo]);
443
444 *stop = YES;
445 return YES;
446 }];
447}
448
449- (void)resetConfForKey:(NSString *)key completion:(void (^__nullable)(BOOL success, NSError * __nullable error))completion {
450 [self sendCommand:TORCommandResetConf arguments:@[key] data:nil observer:^BOOL(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL *stop) {
451 NSUInteger code = codes.firstObject.unsignedIntegerValue;
452 if (code != TORControlReplyCodeOK && code != TORControlReplyCodeBadAuthentication)
453 return NO;
454
455 NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
456 NSDictionary<NSString *, NSString *> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil];
457 BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);
458 if (completion)
459 completion(success, success ? nil : [NSError errorWithDomain:TORControllerErrorDomain code:code userInfo:userInfo]);
460
461 *stop = YES;
462 return YES;
463 }];
464}
465
466- (void)setConfForKey:(NSString *)key withValue:(NSString *)value completion:(void (^__nullable)(BOOL success, NSError * __nullable error))completion {
467 NSString *arg = [NSString stringWithFormat:@"%@=%@", key, value];
468
469 [self sendCommand:TORCommandSetConf arguments:@[arg] data:nil observer:^BOOL(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL *stop) {
470 NSUInteger code = codes.firstObject.unsignedIntegerValue;
471 if (code != TORControlReplyCodeOK && code != TORControlReplyCodeBadAuthentication)
472 return NO;
473
474 NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
475 NSDictionary<NSString *, NSString *> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil];
476 BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);
477 if (completion)
478 completion(success, success ? nil : [NSError errorWithDomain:TORControllerErrorDomain code:code userInfo:userInfo]);
479
480 *stop = YES;
481 return YES;
482 }];
483}
484- (void)setConfs:(NSArray<NSDictionary *> *)configs completion:(void (^__nullable)(BOOL success, NSError * __nullable error))completion {
485 NSMutableArray *conf_arg = [[NSMutableArray alloc] init];
486 for (NSDictionary *config in configs) {
487 NSString *key = [config objectForKey:@"key"];
488 NSString *value = [config objectForKey:@"value"];
489 NSString *arg = [NSString stringWithFormat:@"%@=%@", key, value];
490 [conf_arg addObject:arg];
491 }
492
493 [self sendCommand:TORCommandSetConf arguments:conf_arg data:nil observer:^BOOL(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL *stop) {
494 NSUInteger code = codes.firstObject.unsignedIntegerValue;
495 if (code != TORControlReplyCodeOK && code != TORControlReplyCodeBadAuthentication)
496 return NO;
497
498 NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
499 NSDictionary<NSString *, NSString *> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil];
500 BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);
501 if (completion)
502 completion(success, success ? nil : [NSError errorWithDomain:TORControllerErrorDomain code:code userInfo:userInfo]);
503
504 *stop = YES;
505 return YES;
506 }];
507
508}
509
510- (void)listenForEvents:(NSArray<NSString *> *)events completion:(void (^__nullable)(BOOL success, NSError * __nullable error))completion {
511 [self sendCommand:TORCommandSetEvents arguments:events data:nil observer:^BOOL(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL *stop) {
512 NSUInteger code = codes.firstObject.unsignedIntegerValue;
513 if (code != TORControlReplyCodeOK && code != TORControlReplyCodeUnrecognizedEntity)
514 return NO;
515
516 NSString *message = lines.firstObject ? [[NSString alloc] initWithData:(NSData * _Nonnull)lines.firstObject encoding:NSUTF8StringEncoding] : @"";
517 NSDictionary<NSString *, NSString *> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:message, NSLocalizedDescriptionKey, nil];
518 BOOL success = (code == TORControlReplyCodeOK && [message isEqualToString:@"OK"]);
519 if (success)
520 self->_events = [NSOrderedSet orderedSetWithArray:events];
521 if (completion)
522 completion(success, success ? nil : [NSError errorWithDomain:TORControllerErrorDomain code:code userInfo:userInfo]);
523
524 *stop = YES;
525 return YES;
526 }];
527}
528
529- (void)getInfoForKeys:(NSArray<NSString *> *)keys completion:(void (^)(NSArray<NSString *> *values))completion
530{
531 NSData *ok = [NSData dataWithBytes:"OK" length:2];
532
533 [self sendCommand:TORCommandGetInfo arguments:keys data:nil observer:^BOOL(NSArray<NSNumber *> *codes, NSArray<NSData *> *lines, BOOL *stop) {
534 *stop = YES;
535
536 if (lines.count - 1 != keys.count || codes.count != lines.count)
537 {
538 if (completion)
539 {
540 completion(@[]);
541 }
542
543 return NO;
544 }
545
546 if (codes.lastObject.integerValue != TORControlReplyCodeOK || ![lines.lastObject isEqual:ok])
547 {
548 if (completion)
549 {
550 completion(@[]);
551 }
552
553 return NO;
554 }
555
556 NSMutableDictionary<NSString *, NSString *> *info = [NSMutableDictionary new];
557
558 for (NSUInteger i = 0; i < codes.count - 1; i++)
559 {
560 if (codes[i].integerValue == TORControlReplyCodeOK)
561 {
562 NSString *string = [[NSString alloc] initWithData:lines[i] encoding:NSUTF8StringEncoding];
563
564 if (!string)
565 {
566 completion(@[]);
567
568 return NO;
569 }
570
571 NSRange pos = [string rangeOfString:@"="];
572
573 if (pos.location != NSNotFound)
574 {
575 NSString *key = [[string substringToIndex:pos.location] stringByTrimmingCharactersInSet:
576 NSCharacterSet.doubleQuote];
577
578 if ([keys containsObject:key])
579 {
580 info[key] = [[string substringFromIndex:pos.location + pos.length]
581 stringByTrimmingCharactersInSet:NSCharacterSet.doubleQuote];
582 }
583 else {
584 if (completion)
585 {
586 completion(@[]);
587
588 return NO;
589 }
590 }
591 }
592 }
593 }
594
595 NSMutableArray *values = [NSMutableArray new];
596
597 for (NSString *key in keys)
598 {
599 [values addObject:(info[key] ?: [NSNull null])];
600 }
601
602 if (completion)
603 {
604 completion(values);
605 }
606
607 return YES;
608 }];
609}
610
611- (void)getSessionConfiguration:(void (^)(NSURLSessionConfiguration * __nullable configuration))completion
612{
613 [self getInfoForKeys:@[@"net/listeners/socks"] completion:^(NSArray<NSString *> *values) {
614 if (values.count != 1)
615 return completion(nil);
616
617 NSArray<NSString *> *components = [values.firstObject componentsSeparatedByString:@":"];
618 if (components.count != 2)
619 return completion(nil);
620
621 if ([components[0] isEqualToString:@"unix"])
622 return completion(nil); // TODO: Provide error
623
624 if ([components[1] rangeOfCharacterFromSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]].location != NSNotFound)
625 return completion(nil); // TODO: Provide error
626
627 NSString *host = components[0];
628
629 // Replace 127.0.0.1 with localhost, as without this, there's a strange bug
630 // triggered: It won't resolve .onion addresses, but *only on real devices*.
631 // So, on a real device, there's probably the wrong DNS resolver used, which
632 // would mean, DNS queries were leaking, too.
633 if ([host isEqualToString:@"127.0.0.1"])
634 {
635 host = @"localhost";
636 }
637
638 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
639 configuration.connectionProxyDictionary = @{(id)kCFProxyTypeKey: (id)kCFProxyTypeSOCKS,
640 (id)kCFStreamPropertySOCKSProxyHost: host,
641 (id)kCFStreamPropertySOCKSProxyPort: @([components[1] integerValue])};
642 completion(configuration);
643 }];
644}
645
646- (void)sendCommand:(NSString *)command
647 arguments:(nullable NSArray<NSString *> *)arguments
648 data:(nullable NSData *)data observer:(TORObserverBlock)observer
649{
650 NSParameterAssert(command.length);
651 if (!_channel)
652 {
653 return;
654 }
655
656 if (arguments == nil)
657 {
658 arguments = @[];
659 }
660
661 NSString *argumentsString = [[@[command] arrayByAddingObjectsFromArray:(NSArray * _Nonnull)arguments]
662 componentsJoinedByString:@" "];
663
664 NSMutableData *commandData = [NSMutableData new];
665 if (data.length)
666 {
667 [commandData appendBytes:"+" length:1];
668 }
669 [commandData appendData:(NSData * _Nonnull)[argumentsString dataUsingEncoding:NSUTF8StringEncoding]];
670 [commandData appendBytes:"\r\n" length:2];
671
672 if (data.length)
673 {
674 [commandData appendData:(NSData * _Nonnull)data];
675 [commandData appendBytes:"\r\n.\r\n" length:5];
676 }
677
678 dispatch_data_t dispatchData = dispatch_data_create(commandData.bytes,
679 commandData.length,
680 [self.class controlQueue],
681 DISPATCH_DATA_DESTRUCTOR_DEFAULT);
682
683 dispatch_io_write(_channel, 0, dispatchData, [self.class controlQueue], ^(bool done, dispatch_data_t __unused data, int error) {
684 if (done && !error && observer) {
685 [self->_blocks insertObject:observer atIndex:0];
686 }
687 });
688}
689
690
691- (void)getCircuits:(void (^)(NSArray<TORCircuit *> * _Nonnull circuits))completion
692{
693 [self getInfoForKeys:@[@"circuit-status"] completion:^(NSArray<NSString *> * _Nonnull values) {
694 if (values.count < 1)
695 {
696 if (completion)
697 {
698 completion(@[]);
699 }
700
701 return;
702 }
703
704 NSArray<TORCircuit *> *circuits = [TORCircuit circuitsFromString:(NSString * _Nonnull)values.firstObject];
705
706 NSMutableArray<NSString *> *ipResolveCalls = [NSMutableArray new];
707 NSMutableArray<TORNode *> *map = [NSMutableArray new];
708
709 for (TORCircuit *circuit in circuits)
710 {
711 for (TORNode *node in circuit.nodes)
712 {
713 [ipResolveCalls addObject:[NSString stringWithFormat:@"ns/id/%@", node.fingerprint]];
714 [map addObject:node];
715 }
716 }
717
718 [self getInfoForKeys:ipResolveCalls completion:^(NSArray<NSString *> * _Nonnull values) {
719 for (NSUInteger i = 0; i < values.count; i++) {
720 [map[i] acquireIpAddressesFromNsResponse:values[i]];
721 }
722
723 NSMutableArray<TORNode *> *nodes = [NSMutableArray new];
724
725 for (TORCircuit *circuit in circuits)
726 {
727 [nodes addObjectsFromArray:circuit.nodes];
728 }
729
730 [self resolveCountriesOfNodes:nodes testCapabilities:YES completion:^{
731 if (completion)
732 {
733 completion(circuits);
734 }
735 }];
736 }];
737 }];
738}
739
740- (void)resetConnection:(void (^__nullable)(BOOL success))completion
741{
742 [self sendCommand:TORCommandSignalReload arguments:nil data:nil observer:
743 ^BOOL(NSArray<NSNumber *> * _Nonnull codes, NSArray<NSData *> * _Nonnull __unused lines, BOOL * _Nonnull stop) {
744
745 if (codes.firstObject.integerValue == TORControlReplyCodeOK)
746 {
747 [self sendCommand:TORCommandSignalNewnym arguments:nil data:nil observer:
748 ^BOOL(NSArray<NSNumber *> * _Nonnull codes, NSArray<NSData *> * _Nonnull __unused lines, BOOL * _Nonnull stop) {
749
750 if (completion)
751 {
752 completion(codes.firstObject.integerValue == TORControlReplyCodeOK);
753 }
754
755 *stop = YES;
756 return YES;
757 }];
758 }
759 else {
760 if (completion)
761 {
762 completion(NO);
763 }
764 }
765
766 *stop = YES;
767 return YES;
768 }];
769}
770
771- (void)closeCircuitsByIds:(NSArray<NSString *> *)circuitIds completion:(void (^__nullable)(BOOL success))completion
772{
773 long queueId;
774
775 if (@available(macOS 10.10, *)) {
776 queueId = QOS_CLASS_USER_INITIATED;
777 }
778 else {
779 queueId = DISPATCH_QUEUE_PRIORITY_HIGH;
780 }
781
782 dispatch_async(dispatch_get_global_queue(queueId, 0), ^{
783 __block BOOL success = YES;
784
785 dispatch_group_t group = dispatch_group_create();
786
787 for (NSString *circuitId in circuitIds)
788 {
789 dispatch_group_enter(group);
790
791 [self sendCommand:TORCommandCloseCircuit arguments:@[circuitId] data:nil observer:
792 ^BOOL(NSArray<NSNumber *> * _Nonnull codes, NSArray<NSData *> * _Nonnull __unused lines, BOOL * _Nonnull stop) {
793
794 success = success && codes.firstObject.integerValue == TORControlReplyCodeOK;
795
796 // Need to deparallelize to not mix up responses.
797 dispatch_group_leave(group);
798
799 *stop = YES;
800 return YES;
801 }];
802 }
803
804 dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 10000000000)); // Wait 10 seconds.
805
806 dispatch_async(dispatch_get_main_queue(), ^{
807 if (completion)
808 {
809 completion(success);
810 }
811 });
812 });
813}
814
815- (void)closeCircuits:(NSArray<TORCircuit *> *)circuits completion:(void (^__nullable)(BOOL success))completion
816{
817 NSMutableArray<NSString *> *circuitIds = [NSMutableArray new];
818
819 for (TORCircuit *circuit in circuits)
820 {
821 if (circuit.circuitId.length > 0)
822 {
823 [circuitIds addObject:(NSString * _Nonnull)circuit.circuitId];
824 }
825 }
826
827 [self closeCircuitsByIds:circuitIds completion:completion];
828}
829
830- (void)resolveCountriesOfNodes:(NSArray<TORNode *> * _Nullable)nodes testCapabilities:(BOOL)testCapabilities completion:(void (^__nullable)(void))completion
831{
832 BOOL __block ipv4Available = YES;
833 BOOL __block ipv6Available = YES;
834
835 if (testCapabilities)
836 {
837 dispatch_group_t group = dispatch_group_create();
838 dispatch_group_enter(group);
839
840 [self getInfoForKeys:@[@"ip-to-country/ipv4-available", @"ip-to-country/ipv6-available"] completion:^(NSArray<NSString *> * _Nonnull values) {
841
842 ipv4Available = [values.firstObject isEqualToString:@"1"];
843 ipv6Available = [values.lastObject isEqualToString:@"1"];
844
845 dispatch_group_leave(group);
846 }];
847
848 dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1000000000)); // Wait 1 second.
849 }
850
851 if (!ipv4Available && !ipv6Available)
852 {
853 if (completion)
854 {
855 completion();
856 }
857
858 return;
859 }
860
861 NSMutableArray<NSString *> *geoipResolveCalls = [NSMutableArray new];
862 NSMutableArray<TORNode *> *map = [NSMutableArray new];
863
864 for (TORNode *node in nodes)
865 {
866 if (node.countryCode.length)
867 {
868 continue;
869 }
870
871 if (ipv4Available && node.ipv4Address.length)
872 {
873 [geoipResolveCalls addObject:[NSString stringWithFormat:@"ip-to-country/%@", node.ipv4Address]];
874 [map addObject:node];
875 }
876 else if (ipv6Available && node.ipv6Address.length)
877 {
878 [geoipResolveCalls addObject:[NSString stringWithFormat:@"ip-to-country/%@", node.ipv6Address]];
879 [map addObject:node];
880 }
881 }
882
883 if (geoipResolveCalls.count < 1)
884 {
885 if (completion)
886 {
887 completion();
888 }
889
890 return;
891 }
892
893 [self getInfoForKeys:geoipResolveCalls completion:^(NSArray<NSString *> * _Nonnull values) {
894 for (NSUInteger i = 0; i < values.count; i++)
895 {
896 map[i].countryCode = values[i];
897 }
898
899 if (completion)
900 {
901 completion();
902 }
903 }];
904}
905
906+ (NSArray<NSString *> *)parseStatus:(NSString *)status
907{
908 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^\\s\"]+=\"[^\"]*\"|[^\\s]+" options:0 error:nil];
909
910 NSArray<NSTextCheckingResult *> *matches = [regex matchesInString:status options:0 range:NSMakeRange(0, status.length)];
911
912 NSMutableArray *tokens = [NSMutableArray new];
913
914 for (NSTextCheckingResult *match in matches) {
915 [tokens addObject:[status substringWithRange:match.range]];
916 }
917
918 return tokens;
919}
920
921@end
922
923NS_ASSUME_NONNULL_END