1//
2// TORNode.m
3// Tor
4//
5// Created by Benjamin Erhart on 09.12.19.
6//
7
8#import "TORNode.h"
9#import "NSCharacterSet+PredefinedSets.h"
10
11@implementation TORNode
12
13// MARK: Class Properties
14
15static NSRegularExpression *_ipv4Regex;
16static NSRegularExpression *_ipv6Regex;
17
18+ (NSRegularExpression *)ipv4Regex
19{
20 if (!_ipv4Regex)
21 {
22 _ipv4Regex =
23 [NSRegularExpression
24 regularExpressionWithPattern:@"(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)"
25 options:0 error:nil];
26 }
27
28 return _ipv4Regex;
29}
30
31+ (NSRegularExpression *)ipv6Regex
32{
33 if (!_ipv6Regex)
34 {
35 _ipv6Regex =
36 [NSRegularExpression
37 regularExpressionWithPattern:
38 @"((([\\da-f]{1,4}:){7}([\\da-f]{1,4}|:))|(([\\da-f]{1,4}:){6}(:[\\da-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([\\da-f]{1,4}:){5}(((:[\\da-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([\\da-f]{1,4}:){4}(((:[\\da-f]{1,4}){1,3})|((:[\\da-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([\\da-f]{1,4}:){3}(((:[\\da-f]{1,4}){1,4})|((:[\\da-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([\\da-f]{1,4}:){2}(((:[\\da-f]{1,4}){1,5})|((:[\\da-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([\\da-f]{1,4}:){1}(((:[\\da-f]{1,4}){1,6})|((:[\\da-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[\\da-f]{1,4}){1,7})|((:[\\da-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?"
39 options:NSRegularExpressionCaseInsensitive error:nil];
40 }
41
42 return _ipv6Regex;
43}
44
45// MARK: Class Methods:
46
47+ (NSArray<TORNode *> * _Nonnull)parseFromNsString:(NSString * _Nullable)nsString exitOnly:(BOOL)exitOnly
48{
49 NSMutableArray<TORNode *> *nodes = [NSMutableArray new];
50 NSMutableArray<NSString *> *raw = [NSMutableArray new];
51
52 // A typical NS string for a Tor node might look like this:
53 // (Line breaks are not for readability but contained in original!)
54 //
55 // r ForPrivacyNET ADb6NqtDX9XQ9kBiZjaGfr+3LGg epP7Gxm+NYhwC3V7SPORQCPoVgc 2022-11-18 00:01:48 185.220.101.33 10133 0
56 // a [2a0b:f4c2:2::33]:10133
57 // s Exit Fast Running V2Dir Valid
58 // w Bandwidth=37000
59 //
60 // So, we watch out for a "r" line and add all lines which start with a valid prefix until the
61 // next "r" line to get the full description.
62 // But since we currently don't use the "w" line information, we ignore that as an optimization.
63
64 for (NSString *line in [nsString componentsSeparatedByCharactersInSet:NSCharacterSet.newlineCharacterSet]) {
65 if ([line hasPrefix:@"r"])
66 {
67 if (raw.count > 0)
68 {
69 if (!exitOnly || [raw.lastObject rangeOfString:@"Exit"].location != NSNotFound)
70 {
71 [nodes addObject:[[TORNode alloc] initFromNsString:[raw componentsJoinedByString:@"\n"]]];
72 }
73
74 raw = [[NSMutableArray alloc] initWithObjects:line, nil];
75 }
76 }
77 else if ([line hasPrefix:@"a"] || [line hasPrefix:@"s"])
78 {
79 [raw addObject:line];
80 }
81 }
82
83 if (raw.count > 0 && (!exitOnly || [raw.lastObject rangeOfString:@"Exit"].location != NSNotFound))
84 {
85 [nodes addObject:[[TORNode alloc] initFromNsString:[raw componentsJoinedByString:@"\n"]]];
86 }
87
88 return nodes;
89}
90
91
92// MARK: Initializers
93
94- (instancetype)initFromString:(NSString *)longName
95{
96 self = [super init];
97
98 if (self)
99 {
100 NSArray<NSString *> *components = [longName componentsSeparatedByCharactersInSet:
101 NSCharacterSet.longNameDivider];
102
103 if (components.count > 0)
104 {
105 self.fingerprint = components[0];
106 }
107
108 if (components.count > 1)
109 {
110 self.nickName = components[1];
111 }
112 }
113
114 return self;
115}
116
117- (instancetype)initFromNsString:(NSString *)nsString
118{
119 self = [super init];
120
121 if (self)
122 {
123 NSRange r1 = [nsString rangeOfCharacterFromSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
124
125 if (r1.location != NSNotFound)
126 {
127 NSUInteger p1 = r1.location + r1.length;
128
129 NSRange r2 = [nsString rangeOfCharacterFromSet:NSCharacterSet.whitespaceAndNewlineCharacterSet
130 options:0 range:NSMakeRange(p1, nsString.length - p1)];
131
132 if (r2.location != NSNotFound)
133 {
134 self.nickName = [nsString substringWithRange:NSMakeRange(p1, r2.location - p1)];
135 }
136 }
137
138 [self acquireIpAddressesFromNsResponse:nsString];
139 }
140
141 return self;
142}
143
144
145// MARK: Public Methods
146
147- (void)acquireIpAddressesFromNsResponse:(NSString *)response
148{
149 NSArray<NSTextCheckingResult *> *matches;
150 NSRange range = NSMakeRange(0, response.length);
151
152 matches = [TORNode.ipv4Regex matchesInString:response options:0
153 range:range];
154
155 if (matches.firstObject.numberOfRanges > 0)
156 {
157 self.ipv4Address = [response substringWithRange:[matches.firstObject rangeAtIndex:0]];
158 }
159
160 matches = [TORNode.ipv6Regex matchesInString:response options:0
161 range:range];
162
163 if (matches.firstObject.numberOfRanges > 0)
164 {
165 self.ipv6Address = [response substringWithRange:[matches.firstObject rangeAtIndex:0]];
166 }
167
168 if ([response rangeOfString:@"Exit"].location != NSNotFound)
169 {
170 self.isExit = YES;
171 }
172}
173
174- (NSString *)localizedCountryName
175{
176 if (!self.countryCode)
177 {
178 return nil;
179 }
180
181 NSString *countryCode = (NSString * _Nonnull)self.countryCode;
182 if (@available(iOS 10.0, macOS 10.12, *)) {
183 return [NSLocale.currentLocale localizedStringForCountryCode:countryCode];
184 } else {
185 return [NSLocale.currentLocale displayNameForKey:NSLocaleCountryCode value:countryCode];
186 }
187}
188
189
190// MARK: NSSecureCoding
191
192+ (BOOL)supportsSecureCoding
193{
194 return YES;
195}
196
197- (id)initWithCoder:(NSCoder *)coder
198{
199 if ((self = [super init]))
200 {
201 _fingerprint = [coder decodeObjectOfClass:NSString.class forKey:@"fingerprint"];
202 _nickName = [coder decodeObjectOfClass:NSString.class forKey:@"nickName"];
203 _ipv4Address = [coder decodeObjectOfClass:NSString.class forKey:@"ipv4Address"];
204 _ipv6Address = [coder decodeObjectOfClass:NSString.class forKey:@"ipv6Address"];
205 _countryCode = [coder decodeObjectOfClass:NSString.class forKey:@"countryCode"];
206 }
207
208 return self;
209}
210
211- (void)encodeWithCoder:(NSCoder *)coder
212{
213 [coder encodeObject:self.fingerprint forKey:@"fingerprint"];
214 [coder encodeObject:self.nickName forKey:@"nickName"];
215 [coder encodeObject:self.ipv4Address forKey:@"ipv4Address"];
216 [coder encodeObject:self.ipv6Address forKey:@"ipv6Address"];
217 [coder encodeObject:self.countryCode forKey:@"countryCode"];
218}
219
220
221// MARK: NSObject
222
223- (NSString *)description
224{
225 return [NSString stringWithFormat:@"<%@: %p> fingerprint=%@, nickName=%@, ipv4Address=%@, ipv6Address=%@, countryCode=%@, localizedCountryName=%@",
226 self.class, self, self.fingerprint, self.nickName, self.ipv4Address,
227 self.ipv6Address, self.countryCode, self.localizedCountryName];
228}
229
230- (BOOL)isEqual:(id)other
231{
232 if (other == self)
233 {
234 return YES;
235 }
236
237 if (!other || ![other isKindOfClass:self.class])
238 {
239 return NO;
240 }
241
242 return [self.fingerprint isEqualToString:(NSString * _Nonnull)((TORNode *)other).fingerprint];
243}
244
245- (NSUInteger)hash
246{
247 return self.fingerprint.hash;
248}
249
250@end