react-native-zeroconf 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/NOTICE +0 -3
  2. package/README.md +3 -3
  3. package/android/build.gradle +0 -3
  4. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +1 -1
  5. package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +12 -2
  6. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +129 -4
  7. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +77 -0
  8. package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +654 -0
  9. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +393 -39
  10. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceInfoCallbacks.java +50 -13
  11. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceTypesQuery.java +296 -0
  12. package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +1 -1
  13. package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +59 -22
  14. package/dist/index.js +235 -48
  15. package/index.d.ts +105 -7
  16. package/ios/RNZeroconf/RNZeroconf.m +285 -22
  17. package/package.json +28 -4
  18. package/src/index.js +214 -44
  19. package/windows/ExperimentalFeatures.props +33 -0
  20. package/windows/NuGet.config +11 -0
  21. package/windows/RNZeroconf/RNZeroconf.cpp +228 -0
  22. package/windows/RNZeroconf/RNZeroconf.def +3 -0
  23. package/windows/RNZeroconf/RNZeroconf.h +70 -0
  24. package/windows/RNZeroconf/RNZeroconf.rc +0 -0
  25. package/windows/RNZeroconf/RNZeroconf.vcxproj +150 -0
  26. package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +50 -0
  27. package/windows/RNZeroconf/ReactPackageProvider.cpp +20 -0
  28. package/windows/RNZeroconf/ReactPackageProvider.h +24 -0
  29. package/windows/RNZeroconf/ReactPackageProvider.idl +9 -0
  30. package/windows/RNZeroconf/ZeroconfCore.cpp +1095 -0
  31. package/windows/RNZeroconf/ZeroconfCore.h +167 -0
  32. package/windows/RNZeroconf/pch.cpp +1 -0
  33. package/windows/RNZeroconf/pch.h +30 -0
  34. package/windows/RNZeroconf/resource.h +5 -0
  35. package/windows/RNZeroconf/targetver.h +8 -0
  36. package/windows/RNZeroconf.sln +43 -0
  37. package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +0 -305
  38. package/android/src/main/java/com/github/druk/rx2dnssd/BonjourService.java +0 -400
  39. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2BrowseListener.java +0 -55
  40. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2Dnssd.java +0 -127
  41. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdCommon.java +0 -280
  42. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdEmbedded.java +0 -31
  43. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2QueryListener.java +0 -70
  44. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2RegisterListener.java +0 -47
  45. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2ResolveListener.java +0 -51
@@ -10,6 +10,7 @@
10
10
  #import "RNNetServiceSerializer.h"
11
11
  #import <Network/Network.h>
12
12
  #import <dns_sd.h>
13
+ #import <net/if.h>
13
14
 
14
15
  // dns_sd callbacks are delivered on the main queue, and refs must be deallocated on it
15
16
  #define RNZ_QUEUE dispatch_get_main_queue()
@@ -18,10 +19,19 @@
18
19
 
19
20
  @class RNZScan;
20
21
 
21
- // A found service being resolved: DNSServiceResolve for host, port and TXT, then DNSServiceGetAddrInfo for addresses
22
+ // A found service being resolved: DNSServiceResolve for host, port and TXT, then DNSServiceGetAddrInfo for addresses.
23
+ // Once resolved, the address query and a TXT record query stay open, and changes are emitted again.
24
+ // Without a scan, it is a single resolveService() call settling a promise.
22
25
  @interface RNZResolve : NSObject
23
26
  @property (nonatomic, weak) RNZeroconf *module;
24
27
  @property (nonatomic, weak) RNZScan *scan;
28
+ @property (nonatomic, copy) RCTPromiseResolveBlock promiseResolve;
29
+ @property (nonatomic, copy) RCTPromiseRejectBlock promiseReject;
30
+ @property (nonatomic, assign) uint32_t interfaceIndex;
31
+ @property (nonatomic, assign) NSTimeInterval timeoutSeconds;
32
+ @property (nonatomic, assign) DNSServiceRef txtRef;
33
+ @property (nonatomic, assign) BOOL emitted;
34
+ @property (nonatomic, strong) NSDictionary *lastEmitted;
25
35
  @property (nonatomic, copy) NSString *name;
26
36
  @property (nonatomic, copy) NSString *regtype;
27
37
  @property (nonatomic, copy) NSString *domain;
@@ -46,9 +56,12 @@
46
56
  @property (nonatomic, copy) NSString *scanId;
47
57
  @property (nonatomic, assign) DNSServiceRef browseRef;
48
58
  @property (nonatomic, assign) NSTimeInterval resolveTimeoutSeconds;
59
+ @property (nonatomic, assign) uint32_t interfaceIndex;
49
60
  // Services are reported once per network interface, count them so found/remove are emitted once
50
61
  @property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *foundInterfaces;
51
62
  @property (nonatomic, strong) NSMutableDictionary<NSString *, RNZResolve *> *resolvingServices;
63
+ // Browsing _services._dns-sd._udp: results are service types, reported as "_http._tcp" and not resolved
64
+ @property (nonatomic, assign) BOOL typesOnly;
52
65
  @end
53
66
 
54
67
  @implementation RNZScan
@@ -77,10 +90,13 @@
77
90
  @property (nonatomic, strong, readonly) NSMutableDictionary<NSString *, RNZPublication *> *publishedServices;
78
91
  // Waiting for the register callback
79
92
  @property (nonatomic, strong, readonly) NSMutableSet<RNZPublication *> *pendingPublications;
93
+ // resolveService() calls in flight
94
+ @property (nonatomic, strong, readonly) NSMutableSet<RNZResolve *> *singleResolves;
80
95
 
81
96
  - (void) handleBrowse:(RNZScan *)scan flags:(DNSServiceFlags)flags error:(DNSServiceErrorType)error name:(const char *)name regtype:(const char *)regtype domain:(const char *)domain;
82
97
  - (void) handleResolve:(RNZResolve *)resolve error:(DNSServiceErrorType)error host:(const char *)host port:(uint16_t)port txtLength:(uint16_t)txtLength txt:(const unsigned char *)txt;
83
98
  - (void) handleAddress:(RNZResolve *)resolve flags:(DNSServiceFlags)flags error:(DNSServiceErrorType)error address:(const struct sockaddr *)address;
99
+ - (void) handleTXT:(RNZResolve *)resolve flags:(DNSServiceFlags)flags error:(DNSServiceErrorType)error length:(uint16_t)length data:(const void *)data;
84
100
  - (void) handleRegister:(RNZPublication *)publication flags:(DNSServiceFlags)flags error:(DNSServiceErrorType)error name:(const char *)name;
85
101
 
86
102
  @end
@@ -109,6 +125,14 @@ static void RNZAddressReply(DNSServiceRef ref, DNSServiceFlags flags, uint32_t i
109
125
  [resolve.module handleAddress:resolve flags:flags error:error address:address];
110
126
  }
111
127
 
128
+ static void RNZTXTReply(DNSServiceRef ref, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType error,
129
+ const char *fullName, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata,
130
+ uint32_t ttl, void *context)
131
+ {
132
+ RNZResolve *resolve = (__bridge RNZResolve *)context;
133
+ [resolve.module handleTXT:resolve flags:flags error:error length:rdlen data:rdata];
134
+ }
135
+
112
136
  static void RNZRegisterReply(DNSServiceRef ref, DNSServiceFlags flags, DNSServiceErrorType error,
113
137
  const char *name, const char *regtype, const char *domain, void *context)
114
138
  {
@@ -153,6 +177,7 @@ RCT_EXPORT_MODULE()
153
177
  _scans = [[NSMutableDictionary alloc] init];
154
178
  _publishedServices = [[NSMutableDictionary alloc] init];
155
179
  _pendingPublications = [[NSMutableSet alloc] init];
180
+ _singleResolves = [[NSMutableSet alloc] init];
156
181
  }
157
182
 
158
183
  return self;
@@ -160,28 +185,45 @@ RCT_EXPORT_MODULE()
160
185
 
161
186
  #pragma mark - Scan
162
187
 
188
+ // options: resolveTimeout (seconds), subtype, networkInterface
163
189
  RCT_EXPORT_METHOD(scan:(NSString *)scanId
164
190
  type:(NSString *)type
165
191
  protocol:(NSString *)protocol
166
192
  domain:(NSString *)domain
167
- resolveTimeout:(double)resolveTimeout)
193
+ options:(NSDictionary *)options)
168
194
  {
169
195
  [self stopScan:scanId];
170
196
 
171
197
  RNZScan *scan = [[RNZScan alloc] init];
172
198
  scan.module = self;
173
199
  scan.scanId = scanId;
200
+ double resolveTimeout = [options[@"resolveTimeout"] respondsToSelector:@selector(doubleValue)] ? [options[@"resolveTimeout"] doubleValue] : 0;
174
201
  scan.resolveTimeoutSeconds = resolveTimeout > 0 ? resolveTimeout : 5.0;
175
202
  scan.foundInterfaces = [[NSMutableDictionary alloc] init];
176
203
  scan.resolvingServices = [[NSMutableDictionary alloc] init];
177
204
 
205
+ uint32_t interfaceIndex = kDNSServiceInterfaceIndexAny;
206
+ NSDictionary *interfaceError = [self interfaceIndexForName:options[@"networkInterface"] index:&interfaceIndex];
207
+ if (interfaceError) {
208
+ [self sendError:interfaceError scan:scan];
209
+ return;
210
+ }
211
+ scan.interfaceIndex = interfaceIndex;
212
+
178
213
  NSString *regtype = [NSString stringWithFormat:@"_%@._%@", type, protocol];
214
+ scan.typesOnly = [regtype isEqualToString:@"_services._dns-sd._udp"];
215
+ // "_ipp._tcp,_printer" browses the _printer subtype
216
+ NSString *browseType = regtype;
217
+ NSString *subtype = [RNZeroconf subtypeLabel:options[@"subtype"]];
218
+ if (subtype) {
219
+ browseType = [NSString stringWithFormat:@"%@,%@", regtype, subtype];
220
+ }
179
221
  DNSServiceRef ref = NULL;
180
- DNSServiceErrorType error = DNSServiceBrowse(&ref, 0, kDNSServiceInterfaceIndexAny, regtype.UTF8String,
222
+ DNSServiceErrorType error = DNSServiceBrowse(&ref, 0, interfaceIndex, browseType.UTF8String,
181
223
  domain.length > 0 ? domain.UTF8String : NULL,
182
224
  RNZBrowseReply, (__bridge void *)scan);
183
225
  if (error != kDNSServiceErr_NoError) {
184
- [self sendError:[self errorWithCode:error action:@"Browsing services" serviceName:nil] scan:scan];
226
+ [self sendError:[self browseErrorWithCode:error scan:scan] scan:scan];
185
227
  return;
186
228
  }
187
229
  DNSServiceSetDispatchQueue(ref, RNZ_QUEUE);
@@ -229,7 +271,7 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
229
271
  return;
230
272
  }
231
273
  if (error != kDNSServiceErr_NoError) {
232
- [self sendError:[self errorWithCode:error action:@"Browsing services" serviceName:nil] scan:scan];
274
+ [self sendError:[self browseErrorWithCode:error scan:scan] scan:scan];
233
275
  // The browse can't continue after an error
234
276
  RNZDeallocateLater(scan.browseRef, scan);
235
277
  scan.browseRef = NULL;
@@ -243,12 +285,20 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
243
285
  if (!serviceName) {
244
286
  return;
245
287
  }
288
+ if (scan.typesOnly) {
289
+ // name is "_http" and regtype "_tcp.local.", the service type is "_http._tcp"
290
+ NSString *protocol = [[NSString stringWithUTF8String:regtype] componentsSeparatedByString:@"."].firstObject;
291
+ serviceName = [NSString stringWithFormat:@"%@.%@", serviceName, protocol];
292
+ }
246
293
  NSInteger interfaces = scan.foundInterfaces[serviceName].integerValue;
247
294
 
248
295
  if (flags & kDNSServiceFlagsAdd) {
249
296
  scan.foundInterfaces[serviceName] = @(interfaces + 1);
250
297
  if (interfaces == 0) {
251
298
  [self sendEvent:@"RNZeroconfFound" scan:scan body:@{ kRNServiceKeysName: serviceName }];
299
+ if (scan.typesOnly) {
300
+ return;
301
+ }
252
302
  [self startResolve:serviceName regtype:[NSString stringWithUTF8String:regtype] domain:[NSString stringWithUTF8String:domain] scan:scan];
253
303
  }
254
304
  return;
@@ -279,6 +329,8 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
279
329
  resolve.name = name;
280
330
  resolve.regtype = regtype;
281
331
  resolve.domain = domain;
332
+ resolve.interfaceIndex = scan.interfaceIndex;
333
+ resolve.timeoutSeconds = scan.resolveTimeoutSeconds;
282
334
  scan.resolvingServices[name] = resolve;
283
335
  [self resolve:resolve];
284
336
  }
@@ -289,7 +341,7 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
289
341
  resolve.addresses = [[NSMutableOrderedSet alloc] init];
290
342
 
291
343
  DNSServiceRef ref = NULL;
292
- DNSServiceErrorType error = DNSServiceResolve(&ref, 0, kDNSServiceInterfaceIndexAny, resolve.name.UTF8String,
344
+ DNSServiceErrorType error = DNSServiceResolve(&ref, 0, resolve.interfaceIndex, resolve.name.UTF8String,
293
345
  resolve.regtype.UTF8String, resolve.domain.UTF8String,
294
346
  RNZResolveReply, (__bridge void *)resolve);
295
347
  if (error != kDNSServiceErr_NoError) {
@@ -304,12 +356,12 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
304
356
  resolve.timeoutBlock = dispatch_block_create(0, ^{
305
357
  [weakSelf resolveTimedOut:weakResolve];
306
358
  });
307
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(resolve.scan.resolveTimeoutSeconds * NSEC_PER_SEC)), RNZ_QUEUE, resolve.timeoutBlock);
359
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(resolve.timeoutSeconds * NSEC_PER_SEC)), RNZ_QUEUE, resolve.timeoutBlock);
308
360
  }
309
361
 
310
362
  - (void) resolveTimedOut:(RNZResolve *)resolve
311
363
  {
312
- if (!resolve || resolve.finished || resolve.scan.resolvingServices[resolve.name] != resolve) {
364
+ if (!resolve || resolve.finished || resolve.emitted || ![self isActiveResolve:resolve]) {
313
365
  return;
314
366
  }
315
367
  [self cancelResolve:resolve];
@@ -348,7 +400,7 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
348
400
  resolve.txt = [RNNetServiceSerializer dictionaryFromTXTRecord:txt length:txtLength];
349
401
 
350
402
  DNSServiceRef ref = NULL;
351
- DNSServiceErrorType addressError = DNSServiceGetAddrInfo(&ref, 0, kDNSServiceInterfaceIndexAny,
403
+ DNSServiceErrorType addressError = DNSServiceGetAddrInfo(&ref, 0, resolve.interfaceIndex,
352
404
  kDNSServiceProtocol_IPv4 | kDNSServiceProtocol_IPv6,
353
405
  host, RNZAddressReply, (__bridge void *)resolve);
354
406
  if (addressError != kDNSServiceErr_NoError) {
@@ -376,45 +428,99 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
376
428
  NSString *string = [RNNetServiceSerializer stringFromAddress:address];
377
429
  if (string && (flags & kDNSServiceFlagsAdd)) {
378
430
  [resolve.addresses addObject:string];
431
+ } else if (string) {
432
+ [resolve.addresses removeObject:string];
379
433
  }
380
434
  if (flags & kDNSServiceFlagsMoreComing) {
381
435
  return;
382
436
  }
437
+ [self scheduleEmit:resolve];
438
+ }
383
439
 
384
- // IPv4 and IPv6 answers can arrive in separate batches, wait briefly before emitting
440
+ // Follows the TXT record of a resolved service
441
+ - (void) watchTXT:(RNZResolve *)resolve
442
+ {
443
+ char fullName[kDNSServiceMaxDomainName];
444
+ if (DNSServiceConstructFullName(fullName, resolve.name.UTF8String, resolve.regtype.UTF8String, resolve.domain.UTF8String) != kDNSServiceErr_NoError) {
445
+ return;
446
+ }
447
+ DNSServiceRef ref = NULL;
448
+ if (DNSServiceQueryRecord(&ref, 0, resolve.interfaceIndex, fullName, kDNSServiceType_TXT, kDNSServiceClass_IN,
449
+ RNZTXTReply, (__bridge void *)resolve) != kDNSServiceErr_NoError) {
450
+ return;
451
+ }
452
+ DNSServiceSetDispatchQueue(ref, RNZ_QUEUE);
453
+ resolve.txtRef = ref;
454
+ }
455
+
456
+ - (void) handleTXT:(RNZResolve *)resolve flags:(DNSServiceFlags)flags error:(DNSServiceErrorType)error length:(uint16_t)length data:(const void *)data
457
+ {
458
+ if (resolve.finished || resolve.txtRef == NULL || error != kDNSServiceErr_NoError || !(flags & kDNSServiceFlagsAdd)) {
459
+ return;
460
+ }
461
+ resolve.txt = [RNNetServiceSerializer dictionaryFromTXTRecord:data length:length];
462
+ [self scheduleEmit:resolve];
463
+ }
464
+
465
+ // IPv4 and IPv6 answers can arrive in separate batches, wait briefly before emitting
466
+ - (void) scheduleEmit:(RNZResolve *)resolve
467
+ {
385
468
  if (resolve.emitBlock) {
386
469
  dispatch_block_cancel(resolve.emitBlock);
387
470
  }
388
471
  __weak RNZeroconf *weakSelf = self;
389
472
  __weak RNZResolve *weakResolve = resolve;
390
473
  resolve.emitBlock = dispatch_block_create(0, ^{
391
- [weakSelf finishResolve:weakResolve];
474
+ [weakSelf emitResolve:weakResolve];
392
475
  });
393
476
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), RNZ_QUEUE, resolve.emitBlock);
394
477
  }
395
478
 
396
- - (void) finishResolve:(RNZResolve *)resolve
479
+ - (void) emitResolve:(RNZResolve *)resolve
397
480
  {
398
- if (!resolve || resolve.finished || resolve.addresses.count == 0) {
481
+ if (!resolve || resolve.finished || resolve.addresses.count == 0 || ![self isActiveResolve:resolve]) {
399
482
  return;
400
483
  }
401
- [self cancelResolve:resolve];
402
- if (resolve.scan.resolvingServices[resolve.name] == resolve) {
403
- [resolve.scan.resolvingServices removeObjectForKey:resolve.name];
404
- }
405
-
406
- [self sendEvent:@"RNZeroconfResolved" scan:resolve.scan body:@{
484
+ NSDictionary *body = @{
407
485
  kRNServiceKeysName: resolve.name,
408
486
  kRNServiceKeysFullName: [NSString stringWithFormat:@"%@%@.", resolve.host ?: @"", resolve.regtype],
409
487
  kRNServiceKeysHost: resolve.host ?: @"",
410
488
  kRNServiceKeysPort: @(resolve.port),
411
489
  kRNServiceKeysAddresses: resolve.addresses.array,
412
490
  kRNServiceTxtRecords: resolve.txt ?: @{},
413
- }];
491
+ };
492
+
493
+ if (resolve.promiseResolve) {
494
+ RCTPromiseResolveBlock promiseResolve = resolve.promiseResolve;
495
+ [self endSingleResolve:resolve];
496
+ promiseResolve(body);
497
+ return;
498
+ }
499
+
500
+ if (!resolve.emitted) {
501
+ resolve.emitted = YES;
502
+ if (resolve.timeoutBlock) {
503
+ dispatch_block_cancel(resolve.timeoutBlock);
504
+ resolve.timeoutBlock = nil;
505
+ }
506
+ [self watchTXT:resolve];
507
+ }
508
+ // Emitted again only when something changed
509
+ if ([body isEqualToDictionary:resolve.lastEmitted]) {
510
+ return;
511
+ }
512
+ resolve.lastEmitted = body;
513
+ [self sendEvent:@"RNZeroconfResolved" scan:resolve.scan body:body];
414
514
  }
415
515
 
416
516
  - (void) failResolve:(RNZResolve *)resolve error:(NSDictionary *)error
417
517
  {
518
+ if (resolve.promiseReject) {
519
+ RCTPromiseRejectBlock promiseReject = resolve.promiseReject;
520
+ [self endSingleResolve:resolve];
521
+ [self reject:promiseReject error:error];
522
+ return;
523
+ }
418
524
  [self cancelResolve:resolve];
419
525
  if (resolve.scan.resolvingServices[resolve.name] == resolve) {
420
526
  [resolve.scan.resolvingServices removeObjectForKey:resolve.name];
@@ -422,6 +528,54 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
422
528
  [self sendError:error scan:resolve.scan];
423
529
  }
424
530
 
531
+ - (BOOL) isActiveResolve:(RNZResolve *)resolve
532
+ {
533
+ if (resolve.promiseResolve) {
534
+ return [self.singleResolves containsObject:resolve];
535
+ }
536
+ return resolve.scan.resolvingServices[resolve.name] == resolve;
537
+ }
538
+
539
+ - (void) endSingleResolve:(RNZResolve *)resolve
540
+ {
541
+ [self cancelResolve:resolve];
542
+ resolve.promiseResolve = nil;
543
+ resolve.promiseReject = nil;
544
+ [self.singleResolves removeObject:resolve];
545
+ }
546
+
547
+ // resolveService(): resolves one service by name without scanning. options: timeout (seconds), networkInterface
548
+ RCT_EXPORT_METHOD(resolveService:(NSString *)name
549
+ type:(NSString *)type
550
+ protocol:(NSString *)protocol
551
+ domain:(NSString *)domain
552
+ options:(NSDictionary *)options
553
+ resolve:(RCTPromiseResolveBlock)resolve
554
+ reject:(RCTPromiseRejectBlock)reject)
555
+ {
556
+ uint32_t interfaceIndex = kDNSServiceInterfaceIndexAny;
557
+ NSDictionary *interfaceError = [self interfaceIndexForName:options[@"networkInterface"] index:&interfaceIndex];
558
+ if (interfaceError) {
559
+ [self reject:reject error:interfaceError];
560
+ return;
561
+ }
562
+ double timeout = [options[@"timeout"] respondsToSelector:@selector(doubleValue)] ? [options[@"timeout"] doubleValue] : 0;
563
+
564
+ RNZResolve *single = [[RNZResolve alloc] init];
565
+ single.module = self;
566
+ single.name = name;
567
+ single.regtype = [NSString stringWithFormat:@"_%@._%@", type, protocol];
568
+ single.domain = domain.length > 0 ? domain : @"local.";
569
+ single.interfaceIndex = interfaceIndex;
570
+ single.timeoutSeconds = timeout > 0 ? timeout : 5.0;
571
+ single.promiseResolve = resolve;
572
+ single.promiseReject = reject;
573
+ // The timeout is the caller's, no retry as for scans
574
+ single.retried = YES;
575
+ [self.singleResolves addObject:single];
576
+ [self resolve:single];
577
+ }
578
+
425
579
  // Stops everything in flight for a resolve, safe to call from its own callbacks
426
580
  - (void) cancelResolve:(RNZResolve *)resolve
427
581
  {
@@ -438,6 +592,8 @@ RCT_EXPORT_METHOD(stop:(NSString *)scanId)
438
592
  resolve.resolveRef = NULL;
439
593
  RNZDeallocateLater(resolve.addressRef, resolve);
440
594
  resolve.addressRef = NULL;
595
+ RNZDeallocateLater(resolve.txtRef, resolve);
596
+ resolve.txtRef = NULL;
441
597
  }
442
598
 
443
599
  #pragma mark - Publish
@@ -448,9 +604,18 @@ RCT_EXPORT_METHOD(registerService:(NSString *)type
448
604
  name:(NSString *)name
449
605
  port:(int)port
450
606
  txt:(NSArray<NSArray<NSString *> *> *)txt
607
+ options:(NSDictionary *)options
451
608
  resolve:(RCTPromiseResolveBlock)resolve
452
609
  reject:(RCTPromiseRejectBlock)reject)
453
610
  {
611
+ uint32_t interfaceIndex = kDNSServiceInterfaceIndexAny;
612
+ NSDictionary *interfaceError = [self interfaceIndexForName:options[@"networkInterface"] index:&interfaceIndex];
613
+ if (interfaceError) {
614
+ [self sendError:interfaceError];
615
+ [self reject:reject error:interfaceError];
616
+ return;
617
+ }
618
+
454
619
  NSMutableArray<NSString *> *tooLong = [NSMutableArray array];
455
620
  NSData *txtRecord = [RNNetServiceSerializer TXTRecordFromPairs:txt ?: @[] tooLong:tooLong];
456
621
  for (NSString *key in tooLong) {
@@ -479,9 +644,19 @@ RCT_EXPORT_METHOD(registerService:(NSString *)type
479
644
  publication.resolve = resolve;
480
645
  publication.reject = reject;
481
646
 
647
+ // "_ipp._tcp,_printer,_color" registers the subtypes too
648
+ NSMutableString *registerType = [publication.regtype mutableCopy];
649
+ NSArray *subtypes = [options[@"subtypes"] isKindOfClass:[NSArray class]] ? options[@"subtypes"] : @[];
650
+ for (id subtype in subtypes) {
651
+ NSString *label = [RNZeroconf subtypeLabel:subtype];
652
+ if (label) {
653
+ [registerType appendFormat:@",%@", label];
654
+ }
655
+ }
656
+
482
657
  DNSServiceRef ref = NULL;
483
- DNSServiceErrorType error = DNSServiceRegister(&ref, 0, kDNSServiceInterfaceIndexAny, name.UTF8String,
484
- publication.regtype.UTF8String,
658
+ DNSServiceErrorType error = DNSServiceRegister(&ref, 0, interfaceIndex, name.UTF8String,
659
+ registerType.UTF8String,
485
660
  domain.length > 0 ? domain.UTF8String : NULL,
486
661
  NULL, htons((uint16_t)port),
487
662
  (uint16_t)txtRecord.length, txtRecord.length > 0 ? txtRecord.bytes : NULL,
@@ -565,6 +740,74 @@ RCT_EXPORT_METHOD(unregisterService:(NSString *)serviceName
565
740
  resolve(serviceInfo);
566
741
  }
567
742
 
743
+ // Replaces the TXT record of a published service
744
+ RCT_EXPORT_METHOD(updateService:(NSString *)serviceName
745
+ txt:(NSArray<NSArray<NSString *> *> *)txt
746
+ resolve:(RCTPromiseResolveBlock)resolve
747
+ reject:(RCTPromiseRejectBlock)reject)
748
+ {
749
+ RNZPublication *publication = self.publishedServices[serviceName];
750
+ if (!publication) {
751
+ [self reject:reject error:@{
752
+ @"message": [NSString stringWithFormat:@"Service %@ is not published", serviceName],
753
+ @"code": @"NOT_PUBLISHED",
754
+ @"domain": @"RNZeroconf",
755
+ @"serviceName": serviceName,
756
+ }];
757
+ return;
758
+ }
759
+
760
+ NSMutableArray<NSString *> *tooLong = [NSMutableArray array];
761
+ NSData *txtRecord = [RNNetServiceSerializer TXTRecordFromPairs:txt ?: @[] tooLong:tooLong];
762
+ for (NSString *key in tooLong) {
763
+ [self sendError:@{
764
+ @"message": [NSString stringWithFormat:@"TXT record entry %@ is longer than 255 bytes", key],
765
+ @"code": @"TXT_ENTRY_TOO_LONG",
766
+ @"domain": @"RNZeroconf",
767
+ @"serviceName": serviceName,
768
+ }];
769
+ }
770
+ // An empty TXT record is a single empty string
771
+ static const uint8_t emptyTXT = 0;
772
+ DNSServiceErrorType error = DNSServiceUpdateRecord(publication.ref, NULL, 0,
773
+ txtRecord.length > 0 ? (uint16_t)txtRecord.length : 1,
774
+ txtRecord.length > 0 ? txtRecord.bytes : &emptyTXT, 0);
775
+ if (error != kDNSServiceErr_NoError) {
776
+ [self reject:reject error:[self errorWithCode:error action:@"Updating service" serviceName:serviceName]];
777
+ return;
778
+ }
779
+ publication.txt = [RNNetServiceSerializer dictionaryFromTXTRecord:txtRecord.bytes length:(uint16_t)txtRecord.length];
780
+ resolve([self publicationInfo:publication]);
781
+ }
782
+
783
+ // "printer" or "_printer" -> "_printer"
784
+ + (NSString *) subtypeLabel:(id)subtype
785
+ {
786
+ if (![subtype isKindOfClass:[NSString class]] || [subtype length] == 0) {
787
+ return nil;
788
+ }
789
+ return [subtype hasPrefix:@"_"] ? subtype : [@"_" stringByAppendingString:subtype];
790
+ }
791
+
792
+ // The index of a network interface by name ("en0"), kDNSServiceInterfaceIndexAny without one
793
+ - (NSDictionary *) interfaceIndexForName:(id)name index:(uint32_t *)index
794
+ {
795
+ *index = kDNSServiceInterfaceIndexAny;
796
+ if (![name isKindOfClass:[NSString class]] || [name length] == 0) {
797
+ return nil;
798
+ }
799
+ unsigned int found = if_nametoindex([name UTF8String]);
800
+ if (found == 0) {
801
+ return @{
802
+ @"message": [NSString stringWithFormat:@"Unknown network interface %@", name],
803
+ @"code": @"UNKNOWN_INTERFACE",
804
+ @"domain": @"RNZeroconf",
805
+ };
806
+ }
807
+ *index = found;
808
+ return nil;
809
+ }
810
+
568
811
  - (BOOL) isNameInUse:(NSString *)name
569
812
  {
570
813
  if (self.publishedServices[name]) {
@@ -687,6 +930,10 @@ RCT_EXPORT_METHOD(checkLocalNetworkAccess:(NSString *)type
687
930
  publication.ref = NULL;
688
931
  }
689
932
  [self.pendingPublications removeAllObjects];
933
+ for (RNZResolve *single in [self.singleResolves allObjects]) {
934
+ [self cancelResolve:single];
935
+ }
936
+ [self.singleResolves removeAllObjects];
690
937
  }
691
938
 
692
939
  #pragma mark - Events and errors
@@ -727,6 +974,22 @@ RCT_EXPORT_METHOD(checkLocalNetworkAccess:(NSString *)type
727
974
  reject([NSString stringWithFormat:@"%@", code], error[@"message"], nsError);
728
975
  }
729
976
 
977
+ - (NSDictionary *) browseErrorWithCode:(DNSServiceErrorType)code scan:(RNZScan *)scan
978
+ {
979
+ if (!scan.typesOnly) {
980
+ return [self errorWithCode:code action:@"Browsing services" serviceName:nil];
981
+ }
982
+ if (code == kDNSServiceErr_NoAuth) {
983
+ // Browsing for service types requires the multicast entitlement (Apple's Local Network Privacy FAQ)
984
+ return @{
985
+ @"message": @"Listing service types failed: not authorized, it requires the com.apple.developer.networking.multicast entitlement",
986
+ @"code": @(code),
987
+ @"domain": @"DNSSD",
988
+ };
989
+ }
990
+ return [self errorWithCode:code action:@"Listing service types" serviceName:nil];
991
+ }
992
+
730
993
  - (NSDictionary *) errorWithCode:(DNSServiceErrorType)code action:(NSString *)action serviceName:(NSString *)serviceName
731
994
  {
732
995
  NSString *subject = serviceName ? [NSString stringWithFormat:@"%@ %@", action, serviceName] : action;
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "react-native-zeroconf",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "A Zeroconf discovery utility for react-native",
5
5
  "main": "dist",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
8
  "lint": "eslint src/*.js",
9
9
  "format": "prettier --write src/*.js",
10
- "build": "rm -rf dist && mkdir dist && babel src -o dist/index.js"
10
+ "build": "rm -rf dist && mkdir dist && babel src -o dist/index.js",
11
+ "test": "jest",
12
+ "typecheck": "tsc -p test/types"
11
13
  },
12
14
  "repository": {
13
15
  "type": "git",
@@ -55,6 +57,11 @@
55
57
  "!android/src/main/jni/mdnsresponder/mDNSShared/mDNSResponder.8",
56
58
  "!android/build",
57
59
  "!android/.gradle",
60
+ "windows",
61
+ "!windows/**/*.user",
62
+ "!windows/**/obj",
63
+ "!windows/**/bin",
64
+ "!windows/**/Generated Files",
58
65
  "ios",
59
66
  "react-native-zeroconf.podspec",
60
67
  "NOTICE"
@@ -87,9 +94,14 @@
87
94
  "@babel/core": "^7.23.0",
88
95
  "@babel/preset-env": "^7.23.0",
89
96
  "@babel/preset-flow": "^7.23.0",
97
+ "babel-jest": "^29.6.3",
90
98
  "eslint": "^8.57.0",
91
99
  "eslint-config-prettier": "^9.1.0",
92
- "prettier": "^3.2.0"
100
+ "jest": "^29.6.3",
101
+ "prettier": "^3.2.0",
102
+ "react": "18.2.0",
103
+ "react-test-renderer": "18.2.0",
104
+ "typescript": "^5.4.0"
93
105
  },
94
106
  "babel": {
95
107
  "presets": [
@@ -104,5 +116,17 @@
104
116
  "@babel/preset-flow"
105
117
  ]
106
118
  },
107
- "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
119
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
120
+ "jest": {
121
+ "roots": [
122
+ "<rootDir>/test"
123
+ ],
124
+ "testEnvironment": "node",
125
+ "testMatch": [
126
+ "**/*.test.js"
127
+ ],
128
+ "moduleNameMapper": {
129
+ "^react-native$": "<rootDir>/test/helpers/mockReactNative.js"
130
+ }
131
+ }
108
132
  }