this.onBackdropClick(event)} ref={this.refModal}>
+
this.onBackdropClick(event)}
+ ref={this.refModal}
+ >
{
- const parameters = {};
- parameters["name"] = address;
- parameters["type"] = type;
- parameters["cd"] = false; /* check disabled */
- parameters["do"] = true; /* DNSSEC info */
-
- const parameter_string = Object.keys(parameters).reduceRight((a, b) => a + "&" + b + "=" + encodeURIComponent(parameters[b]));
- const response = await fetch("https://dns.google/resolve?" + parameter_string, {
- method: "GET"
- });
- if(response.status !== 200)
- throw response.statusText || tr("server returned ") + response.status;
-
- let response_string = "unknown";
- let response_data: DNSResponse;
- try {
- response_string = await response.text();
- response_data = JSON.parse(response_string);
- } catch(ex) {
- log.error(LogCategory.DNS, tr("Failed to parse response data: %o. Data: %s"), ex, response_string);
- throw "failed to parse response";
- }
-
- if(response_data.TC)
- throw "truncated response";
-
- if(response_data.Status !== ErrorCode.NOERROR) {
- if(response_data.Status === ErrorCode.NXDOMAIN)
- return [];
- throw "dns error code " + response_data.Status;
- }
-
- log.trace(LogCategory.DNS, tr("Result for query %s (%s): %o"), address, RRType[type], response_data);
-
- if(!response_data.Answer) return [];
- return response_data.Answer.filter(e => (e.name === address || e.name === address + ".") && e.type === type);
-}
-
-type Address = { host: string, port: number };
-
-interface DNSResolveMethod {
- name() : string;
- resolve(address: Address) : Promise;
-}
-
-class IPResolveMethod implements DNSResolveMethod {
- readonly v6: boolean;
-
- constructor(v6: boolean) {
- this.v6 = v6;
- }
-
-
- name(): string {
- return "ip v" + (this.v6 ? "6" : "4") + " resolver";
- }
-
- resolve(address: Address): Promise {
- return resolve(address.host, this.v6 ? RRType.AAAA : RRType.A).then(e => {
- if(!e.length) return undefined;
-
- return {
- host: e[0].data,
- port: address.port
- }
- });
- }
-}
-
-type ParsedSVRRecord = {
- target: string;
- port: number;
-
- priority: number;
- weight: number;
-}
-class SRVResolveMethod implements DNSResolveMethod {
- readonly application: string;
-
- constructor(app: string) {
- this.application = app;
- }
-
- name(): string {
- return "srv resolve [" + this.application + "]";
- }
-
- resolve(address: Address): Promise {
- return resolve((this.application ? this.application + "." : "") + address.host, RRType.SRV).then(e => {
- if(!e) return undefined;
-
- const records: {[key: number]:ParsedSVRRecord[]} = {};
- for(const record of e) {
- const parts = record.data.split(" ");
- if(parts.length !== 4) {
- log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record);
- continue;
- }
-
- const priority = parseInt(parts[0]);
- const weight = parseInt(parts[1]);
- const port = parseInt(parts[2]);
-
- if((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) {
- log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record);
- continue;
- }
-
- (records[priority] || (records[priority] = [])).push({
- priority: priority,
- weight: weight,
- port: port,
- target: parts[3]
- });
- }
-
- /* get the record with the highest priority */
- const priority_strings = Object.keys(records);
- if(!priority_strings.length) return undefined;
-
- let highest_priority: ParsedSVRRecord[];
- for(const priority_str of priority_strings) {
- if(!highest_priority || !highest_priority.length)
- highest_priority = records[priority_str];
-
- if(highest_priority[0].priority < parseInt(priority_str))
- highest_priority = records[priority_str];
- }
-
- if(!highest_priority.length) return undefined;
-
- /* select randomly one record */
- let record: ParsedSVRRecord;
- const max_weight = highest_priority.map(e => e.weight).reduce((a, b) => a + b, 0);
- if(max_weight == 0) record = highest_priority[Math.floor(Math.random() * highest_priority.length)];
- else {
- let rnd = Math.random() * max_weight;
- for(let i = 0; i < highest_priority.length; i++) {
- rnd -= highest_priority[i].weight;
- if(rnd > 0) continue;
-
- record = highest_priority[i];
- break;
- }
- }
- if(!record) /* shall never happen */
- record = highest_priority[0];
- return {
- host: record.target,
- port: record.port == 0 ? address.port : record.port
- };
- });
- }
-}
-
-class SRV_IPResolveMethod implements DNSResolveMethod {
- readonly srv_resolver: DNSResolveMethod;
- readonly ipv4_resolver: IPResolveMethod;
- readonly ipv6_resolver: IPResolveMethod;
-
- constructor(srv_resolver: DNSResolveMethod, ipv4_resolver: IPResolveMethod, ipv6_resolver: IPResolveMethod) {
- this.srv_resolver = srv_resolver;
- this.ipv4_resolver = ipv4_resolver;
- this.ipv6_resolver = ipv6_resolver;
- }
-
- name(): string {
- return "srv ip resolver [" + this.srv_resolver.name() + "; " + this.ipv4_resolver.name() + "; " + this.ipv6_resolver.name() + "]";
- }
-
- resolve(address: Address): Promise {
- return this.srv_resolver.resolve(address).then(e => {
- if(!e) return undefined;
-
- return this.ipv4_resolver.resolve(e).catch(() => this.ipv6_resolver.resolve(e));
- });
- }
-}
-
-class DomainRootResolveMethod implements DNSResolveMethod {
- readonly resolver: DNSResolveMethod;
-
- constructor(resolver: DNSResolveMethod) {
- this.resolver = resolver;
- }
-
- name(): string {
- return "domain-root [" + this.resolver.name() + "]";
- }
-
- resolve(address: Address): Promise {
- const parts = address.host.split(".");
- if(parts.length < 3) return undefined;
-
- return this.resolver.resolve({
- host: parts.slice(-2).join("."),
- port: address.port
- });
- }
-}
-
-class TeaSpeakDNSResolve {
- readonly address: Address;
- private resolvers: {[key: string]:{resolver: DNSResolveMethod, after: string[]}} = {};
- private resolving = false;
- private timeout;
-
- private callback_success;
- private callback_fail;
-
- private finished_resolvers: string[];
- private resolving_resolvers: string[];
-
- constructor(addr: Address) {
- this.address = addr;
- }
-
- register_resolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) {
- if(this.resolving) throw tr("resolver is already resolving");
-
- this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) };
- }
-
- resolve(timeout: number) : Promise {
- if(this.resolving) throw tr("already resolving");
- this.resolving = true;
-
- this.finished_resolvers = [];
- this.resolving_resolvers = [];
-
- const cleanup = () => {
- clearTimeout(this.timeout);
- this.resolving = false;
- };
-
- this.timeout = setTimeout(() => {
- this.callback_fail(tr("timeout"));
- }, timeout);
- log.trace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.host, this.address.port);
-
- return new Promise((resolve, reject) => {
- this.callback_success = data => {
- cleanup();
- resolve(data);
- };
-
- this.callback_fail = error => {
- cleanup();
- reject(error);
- };
-
- this.invoke_resolvers();
- });
- }
-
- private invoke_resolvers() {
- let invoke_count = 0;
-
- _main_loop:
- for(const resolver_name of Object.keys(this.resolvers)) {
- if(this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
- if(this.finished_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
-
- const resolver = this.resolvers[resolver_name];
- for(const after of resolver.after)
- if(this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop;
-
- invoke_count++;
- log.trace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name);
-
- this.resolving_resolvers.push(resolver_name);
- resolver.resolver.resolve(this.address).then(result => {
- if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */
- this.finished_resolvers.push(resolver_name);
-
- if(!result) {
- log.trace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name);
- this.invoke_resolvers();
- return;
- }
-
- log.trace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"),
- this.address.host, this.address.port,
- result.host, result.port,
- resolver_name);
- this.callback_success(result);
- }).catch(error => {
- if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */
- this.finished_resolvers.push(resolver_name);
-
- log.trace(LogCategory.DNS, tr(" Resolver %s ran into an error: %o"), resolver_name, error);
- this.invoke_resolvers();
- }).then(() => {
- this.resolving_resolvers.remove(resolver_name);
- if(!this.resolving_resolvers.length && this.resolving)
- this.invoke_resolvers();
- });
- }
-
- if(invoke_count === 0 && !this.resolving_resolvers.length && this.resolving)
- this.callback_fail("no response");
- }
-}
-
-const resolver_ip_v4 = new IPResolveMethod(false);
-const resolver_ip_v6 = new IPResolveMethod(true);
-
-const resolver_srv_ts = new SRV_IPResolveMethod(new SRVResolveMethod("_ts._udp"), resolver_ip_v4, resolver_ip_v6);
-const resolver_srv_ts3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp"), resolver_ip_v4, resolver_ip_v6);
-
-const resolver_dr_srv_ts = new DomainRootResolveMethod(resolver_srv_ts);
-const resolver_dr_srv_ts3 = new DomainRootResolveMethod(resolver_srv_ts3);
+import {resolveAddressIpv4, resolveTeaSpeakServerAddress} from "tc-backend/web/dns/resolver";
export function supported() { return true; }
-export async function resolve_address(address: ServerAddress, _options?: ResolveOptions) : Promise {
- const options = Object.assign({}, default_options);
- Object.assign(options, _options);
-
- const resolver = new TeaSpeakDNSResolve(address);
-
- resolver.register_resolver(resolver_srv_ts);
- resolver.register_resolver(resolver_srv_ts3);
- //TODO: TSDNS somehow?
-
- resolver.register_resolver(resolver_dr_srv_ts, resolver_srv_ts);
- resolver.register_resolver(resolver_dr_srv_ts3, resolver_srv_ts3);
-
- resolver.register_resolver(resolver_ip_v4, resolver_srv_ts, resolver_srv_ts3);
- resolver.register_resolver(resolver_ip_v6, resolver_ip_v4);
-
- const response = await resolver.resolve(options.timeout || 5000);
- return {
- target_ip: response.host,
- target_port: response.port
- };
+export async function resolve_address(address: ServerAddress, options?: ResolveOptions) : Promise {
+ return await resolveTeaSpeakServerAddress(address, options);
}
export async function resolve_address_ipv4(address: string) : Promise {
- const result = await resolve(address, RRType.A);
- if(!result.length) return undefined;
-
- return result[0].data;
+ return await resolveAddressIpv4(address);
}
\ No newline at end of file
diff --git a/web/app/dns/api.ts b/web/app/dns/api.ts
new file mode 100644
index 00000000..d822a017
--- /dev/null
+++ b/web/app/dns/api.ts
@@ -0,0 +1,188 @@
+import {tr} from "tc-shared/i18n/localize";
+import * as log from "tc-shared/log";
+import {LogCategory, logTrace} from "tc-shared/log";
+
+export enum RRType {
+ A = 1, // a host address,[RFC1035],
+ NS = 2, // an authoritative name server,[RFC1035],
+ MD = 3, // a mail destination (OBSOLETE - use MX),[RFC1035],
+ MF = 4, // a mail forwarder (OBSOLETE - use MX),[RFC1035],
+ CNAME = 5, // the canonical name for an alias,[RFC1035],
+ SOA = 6, // marks the start of a zone of authority,[RFC1035],
+ MB = 7, // a mailbox domain name (EXPERIMENTAL),[RFC1035],
+ MG = 8, // a mail group member (EXPERIMENTAL),[RFC1035],
+ MR = 9, // a mail rename domain name (EXPERIMENTAL),[RFC1035],
+ NULL_ = 10, // a null RR (EXPERIMENTAL),[RFC1035],
+ WKS = 11, // a well known service description,[RFC1035],
+ PTR = 12, // a domain name pointer,[RFC1035],
+ HINFO = 13, // host information,[RFC1035],
+ MINFO = 14, // mailbox or mail list information,[RFC1035],
+ MX = 15, // mail exchange,[RFC1035],
+ TXT = 16, // text strings,[RFC1035],
+ RP = 17, // for Responsible Person,[RFC1183],
+ AFSDB = 18, // for AFS Data Base location,[RFC1183][RFC5864],
+ X25 = 19, // for X.25 PSDN address,[RFC1183],
+ ISDN = 20, // for ISDN address,[RFC1183],
+ RT = 21, // for Route Through,[RFC1183],
+ NSAP = 22, // "for NSAP address, NSAP style A record",[RFC1706],
+ NSAP_PTR = 23, // "for domain name pointer, NSAP style",[RFC1348][RFC1637][RFC1706],
+ SIG = 24, // for security signature,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2931][RFC3110][RFC3008],
+ KEY = 25, // for security key,[RFC4034][RFC3755][RFC2535][RFC2536][RFC2537][RFC2539][RFC3008][RFC3110],
+ PX = 26, // X.400 mail mapping information,[RFC2163],
+ GPOS = 27, // Geographical Position,[RFC1712],
+ AAAA = 28, // IP6 Address,[RFC3596],
+ LOC = 29, // Location Information,[RFC1876],
+ NXT = 30, // Next Domain (OBSOLETE),[RFC3755][RFC2535],
+ EID = 31, // Endpoint Identifier,[Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt],
+ NIMLOC = 32, // Nimrod Locator,[1][Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt],
+ SRV = 33, // Server Selection,[1][RFC2782],
+ ATMA = 34, // ATM Address,"[ ATM Forum Technical Committee, ""ATM Name System, V2.0"", Doc ID: AF-DANS-0152.000, July 2000. Available from and held in escrow by IANA.]",
+ NAPTR = 35, // Naming Authority Pointer,[RFC2915][RFC2168][RFC3403],
+ KX = 36, // Key Exchanger,[RFC2230],
+ CERT = 37, //CERT, // [RFC4398],
+ A6 = 38, // A6 (OBSOLETE - use AAAA),[RFC3226][RFC2874][RFC6563],
+ DNAME = 39, //DNAME, // [RFC6672],
+ SINK = 40, //SINK, // [Donald_E_Eastlake][http://tools.ietf.org/html/draft-eastlake-kitchen-sink],
+ OPT = 41, //OPT, // [RFC6891][RFC3225],
+ APL = 42, //APL, // [RFC3123],
+ DS = 43, // Delegation Signer,[RFC4034][RFC3658],
+ SSHFP = 44, // SSH Key Fingerprint,[RFC4255],
+ IPSECKEY = 45, //IPSECKEY, // [RFC4025],
+ RRSIG = 46, //RRSIG, // [RFC4034][RFC3755],
+ NSEC = 47, //NSEC, // [RFC4034][RFC3755],
+ DNSKEY = 48, //DNSKEY, // [RFC4034][RFC3755],
+ DHCID = 49, //DHCID, // [RFC4701],
+ NSEC3 = 50, //NSEC3, // [RFC5155],
+ NSEC3PARAM = 51, //NSEC3PARAM, // [RFC5155],
+ TLSA = 52, //TLSA, // [RFC6698],
+ SMIMEA = 53, // S/MIME cert association,[RFC8162],SMIMEA/smimea-completed-template
+ Unassigned = 54, // ,
+ HIP = 55, // Host Identity Protocol,[RFC8005],
+ NINFO = 56, //NINFO [Jim_Reid], // NINFO/ninfo-completed-template
+ RKEY = 57, //RKEY [Jim_Reid], // RKEY/rkey-completed-template
+ TALINK = 58, // Trust Anchor LINK,[Wouter_Wijngaards],TALINK/talink-completed-template
+ CDS = 59, // Child DS,[RFC7344],CDS/cds-completed-template
+ CDNSKEY = 60, // DNSKEY(s) the Child wants reflected in DS,[RFC7344],
+ OPENPGPKEY = 61, // OpenPGP Key,[RFC7929],OPENPGPKEY/openpgpkey-completed-template
+ CSYNC = 62, // Child-To-Parent Synchronization,[RFC7477],
+ ZONEMD = 63, // message digest for DNS zone,[draft-wessels-dns-zone-digest],ZONEMD/zonemd-completed-template
+ //Unassigned = 64-98,
+ SPF = 99, // [RFC7208],
+ UINFO = 100, // [IANA-Reserved],
+ UID = 101, // [IANA-Reserved],
+ GID = 102, // [IANA-Reserved],
+ UNSPEC = 103, // [IANA-Reserved],
+ NID = 104, //[RFC6742], // ILNP/nid-completed-template
+ L32 = 105, //[RFC6742], // ILNP/l32-completed-template
+ L64 = 106, //[RFC6742], // ILNP/l64-completed-template
+ LP = 107, //[RFC6742], // ILNP/lp-completed-template
+ EUI48 = 108, // an EUI-48 address,[RFC7043],EUI48/eui48-completed-template
+ EUI64 = 109, // an EUI-64 address,[RFC7043],EUI64/eui64-completed-template
+ //Unassigned = 110-248, // ,
+ TKEY = 249, // Transaction Key,[RFC2930],
+ TSIG = 250, // Transaction Signature,[RFC2845],
+ IXFR = 251, // incremental transfer,[RFC1995],
+ AXFR = 252, // transfer of an entire zone,[RFC1035][RFC5936],
+ MAILB = 253, // "mailbox-related RRs (MB, MG or MR)",[RFC1035],
+ MAILA = 254, // mail agent RRs (OBSOLETE - see MX),[RFC1035],
+ ANY = 255, // A request for some or all records the server has available,[RFC1035][RFC6895][RFC8482],
+ URI = 256, //URI [RFC7553], // URI/uri-completed-template
+ CAA = 257, // Certification Authority Restriction,[RFC-ietf-lamps-rfc6844bis-07],CAA/caa-completed-template
+ AVC = 258, // Application Visibility and Control,[Wolfgang_Riedel],AVC/avc-completed-template
+ DOA = 259, // Digital Object Architecture,[draft-durand-doa-over-dns],DOA/doa-completed-template
+ AMTRELAY = 260, // Automatic Multicast Tunneling Relay,[draft-ietf-mboned-driad-amt-discovery],AMTRELAY/amtrelay-completed-template
+ //Unassigned = 261-32767,
+ TA = 32768, // DNSSEC Trust Authorities,"[Sam_Weiler][http://cameo.library.cmu.edu/][ Deploying DNSSEC Without a Signed Root. Technical Report 1999-19,
+ // Information Networking Institute, Carnegie Mellon University, April 2004.]",
+ DLV = 32769, // DNSSEC Lookaside Validation,[RFC4431],
+ //Unassigned = 32770-65279,, // ,
+ //Private use,65280-65534,,,,
+ Reserved = 65535,
+}
+export enum ErrorCode {
+ NOERROR = 0,
+ FORMERR = 1,
+ SERVFAIL = 2,
+ NXDOMAIN = 3,
+ NOTIMP = 4,
+ REFUSED = 5,
+ YXDOMAIN = 6,
+ XRRSET = 7,
+ NOTAUTH = 8,
+ NOTZONE = 9
+}
+
+interface DNSAnswer {
+ name: string;
+ type: RRType;
+ TTL: null;
+ data: string;
+}
+
+interface DNSQuery {
+ name: string;
+ type: RRType;
+}
+
+interface DNSResponse {
+ Status: ErrorCode;
+ Comment: string;
+
+ TC: boolean; /* truncated */
+ RD: true;
+ RA: true;
+ AD: boolean; /* DNSSEC valid */
+ CD: boolean; /* client DNSSEC disabled */
+
+ Question: DNSQuery[];
+ Answer?: DNSAnswer[];
+ Authority?: DNSAnswer[];
+ Additional: any[];
+}
+
+export async function executeDnsRequest(address: string, type: RRType) : Promise {
+ const parameters = {};
+ parameters["name"] = address;
+ parameters["type"] = type;
+ parameters["cd"] = false; /* check disabled */
+ parameters["do"] = true; /* DNSSEC info */
+
+ const parameter_string = Object.keys(parameters).reduceRight((a, b) => a + "&" + b + "=" + encodeURIComponent(parameters[b]));
+ const response = await fetch("https://dns.google/resolve?" + parameter_string, {
+ method: "GET"
+ });
+
+ if(response.status !== 200) {
+ throw response.statusText || tr("server returned ") + response.status;
+ }
+
+ let response_string = "unknown";
+ let responseData: DNSResponse;
+ try {
+ response_string = await response.text();
+ responseData = JSON.parse(response_string);
+ } catch(ex) {
+ log.error(LogCategory.DNS, tr("Failed to parse response data: %o. Data: %s"), ex, response_string);
+ throw "failed to parse response";
+ }
+
+ if(responseData.TC) {
+ throw "truncated response";
+ }
+
+ if(responseData.Status !== ErrorCode.NOERROR) {
+ if(responseData.Status === ErrorCode.NXDOMAIN) {
+ return [];
+ }
+
+ throw "dns error code " + responseData.Status;
+ }
+
+ logTrace(LogCategory.DNS, tr("Result for query %s (%s): %o"), address, RRType[type], responseData);
+
+ if(!responseData.Answer) {
+ return [];
+ }
+
+ return responseData.Answer.filter(e => (e.name === address || e.name === address + ".") && e.type === type);
+}
\ No newline at end of file
diff --git a/web/app/dns/resolver.ts b/web/app/dns/resolver.ts
new file mode 100644
index 00000000..17b0a2de
--- /dev/null
+++ b/web/app/dns/resolver.ts
@@ -0,0 +1,357 @@
+import * as log from "tc-shared/log";
+import {LogCategory, logTrace} from "tc-shared/log";
+import {tr} from "tc-shared/i18n/localize";
+import {ServerAddress} from "tc-shared/tree/Server";
+import {AddressTarget, default_options, ResolveOptions} from "tc-shared/dns";
+import {executeDnsRequest, RRType} from "tc-backend/web/dns/api";
+
+type Address = { host: string, port: number };
+
+interface DNSResolveMethod {
+ name() : string;
+ resolve(address: Address) : Promise;
+}
+
+class LocalhostResolver implements DNSResolveMethod {
+ name(): string {
+ return "localhost";
+ }
+
+ async resolve(address: Address): Promise {
+ if(address.host === "localhost") {
+ return {
+ host: "127.0.0.1",
+ port: address.port
+ }
+ }
+
+ return undefined;
+ }
+
+}
+
+class IPResolveMethod implements DNSResolveMethod {
+ readonly v6: boolean;
+
+ constructor(v6: boolean) {
+ this.v6 = v6;
+ }
+
+
+ name(): string {
+ return "ip v" + (this.v6 ? "6" : "4") + " resolver";
+ }
+
+ async resolve(address: Address): Promise {
+ const answer = await executeDnsRequest(address.host, this.v6 ? RRType.AAAA : RRType.A);
+ if(!answer.length) {
+ return undefined;
+ }
+
+ return {
+ host: answer[0].data,
+ port: address.port
+ }
+ }
+}
+
+type ParsedSVRRecord = {
+ target: string;
+ port: number;
+
+ priority: number;
+ weight: number;
+}
+class SRVResolveMethod implements DNSResolveMethod {
+ readonly application: string;
+
+ constructor(app: string) {
+ this.application = app;
+ }
+
+ name(): string {
+ return "srv resolve [" + this.application + "]";
+ }
+
+ async resolve(address: Address): Promise {
+ const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.host, RRType.SRV);
+
+ const records: {[key: number]: ParsedSVRRecord[]} = {};
+ for(const record of answer) {
+ const parts = record.data.split(" ");
+ if(parts.length !== 4) {
+ log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record);
+ continue;
+ }
+
+ const priority = parseInt(parts[0]);
+ const weight = parseInt(parts[1]);
+ const port = parseInt(parts[2]);
+
+ if((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) {
+ log.warn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record);
+ continue;
+ }
+
+ (records[priority] || (records[priority] = [])).push({
+ priority: priority,
+ weight: weight,
+ port: port,
+ target: parts[3]
+ });
+ }
+
+ /* get the record with the highest priority */
+ const priority_strings = Object.keys(records);
+ if(!priority_strings.length) {
+ return undefined;
+ }
+
+ let highestPriority: ParsedSVRRecord[];
+ for(const priority_str of priority_strings) {
+ if(!highestPriority || !highestPriority.length) {
+ highestPriority = records[priority_str];
+ }
+
+ if(highestPriority[0].priority < parseInt(priority_str)) {
+ highestPriority = records[priority_str];
+ }
+ }
+
+ if(!highestPriority.length) {
+ return undefined;
+ }
+
+ /* select randomly one record */
+ let record: ParsedSVRRecord;
+ const max_weight = highestPriority.map(e => e.weight).reduce((a, b) => a + b, 0);
+ if(max_weight == 0) {
+ record = highestPriority[Math.floor(Math.random() * highestPriority.length)];
+ } else {
+ let rnd = Math.random() * max_weight;
+ for(let i = 0; i < highestPriority.length; i++) {
+ rnd -= highestPriority[i].weight;
+ if(rnd > 0) {
+ continue;
+ }
+
+ record = highestPriority[i];
+ break;
+ }
+ }
+
+ if(!record) {
+ /* shall never happen */
+ record = highestPriority[0];
+ }
+
+ return {
+ host: record.target,
+ port: record.port == 0 ? address.port : record.port
+ };
+ }
+}
+
+class SRV_IPResolveMethod implements DNSResolveMethod {
+ readonly srvResolver: DNSResolveMethod;
+ readonly ipv4Resolver: IPResolveMethod;
+ readonly ipv6Resolver: IPResolveMethod;
+
+ constructor(srv_resolver: DNSResolveMethod, ipv4Resolver: IPResolveMethod, ipv6Resolver: IPResolveMethod) {
+ this.srvResolver = srv_resolver;
+ this.ipv4Resolver = ipv4Resolver;
+ this.ipv6Resolver = ipv6Resolver;
+ }
+
+ name(): string {
+ return "srv ip resolver [" + this.srvResolver.name() + "; " + this.ipv4Resolver.name() + "; " + this.ipv6Resolver.name() + "]";
+ }
+
+ async resolve(address: Address): Promise {
+ const srvAddress = await this.srvResolver.resolve(address);
+ if(!srvAddress) {
+ return undefined;
+ }
+
+ try {
+ return await this.ipv4Resolver.resolve(srvAddress);
+ } catch (_error) {
+ return await this.ipv6Resolver.resolve(srvAddress);
+ }
+ }
+}
+
+class DomainRootResolveMethod implements DNSResolveMethod {
+ readonly resolver: DNSResolveMethod;
+
+ constructor(resolver: DNSResolveMethod) {
+ this.resolver = resolver;
+ }
+
+ name(): string {
+ return "domain-root [" + this.resolver.name() + "]";
+ }
+
+ async resolve(address: Address): Promise {
+ const parts = address.host.split(".");
+ if(parts.length < 3) {
+ return undefined;
+ }
+
+ return await this.resolver.resolve({
+ host: parts.slice(-2).join("."),
+ port: address.port
+ });
+ }
+}
+
+class TeaSpeakDNSResolve {
+ readonly address: Address;
+ private resolvers: {[key: string]:{ resolver: DNSResolveMethod, after: string[] }} = {};
+ private resolving = false;
+ private timeout;
+
+ private callback_success;
+ private callback_fail;
+
+ private finished_resolvers: string[];
+ private resolving_resolvers: string[];
+
+ constructor(addr: Address) {
+ this.address = addr;
+ }
+
+ registerResolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) {
+ if(this.resolving) {
+ throw tr("resolver is already resolving");
+ }
+
+ this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) };
+ }
+
+ resolve(timeout: number) : Promise {
+ if(this.resolving) {
+ throw tr("already resolving");
+ }
+ this.resolving = true;
+
+ this.finished_resolvers = [];
+ this.resolving_resolvers = [];
+
+ const cleanup = () => {
+ clearTimeout(this.timeout);
+ this.resolving = false;
+ };
+
+ this.timeout = setTimeout(() => {
+ this.callback_fail(tr("timeout"));
+ }, timeout);
+ logTrace(LogCategory.DNS, tr("Start resolving %s:%d"), this.address.host, this.address.port);
+
+ return new Promise((resolve, reject) => {
+ this.callback_success = data => {
+ cleanup();
+ resolve(data);
+ };
+
+ this.callback_fail = error => {
+ cleanup();
+ reject(error);
+ };
+
+ this.invoke_resolvers();
+ });
+ }
+
+ private invoke_resolvers() {
+ let invoke_count = 0;
+
+ _main_loop:
+ for(const resolver_name of Object.keys(this.resolvers)) {
+ if(this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
+ if(this.finished_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
+
+ const resolver = this.resolvers[resolver_name];
+ for(const after of resolver.after)
+ if(this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop;
+
+ invoke_count++;
+ log.trace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name);
+
+ this.resolving_resolvers.push(resolver_name);
+ resolver.resolver.resolve(this.address).then(result => {
+ if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */
+ this.finished_resolvers.push(resolver_name);
+
+ if(!result) {
+ log.trace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name);
+ this.invoke_resolvers();
+ return;
+ }
+
+ log.trace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"),
+ this.address.host, this.address.port,
+ result.host, result.port,
+ resolver_name);
+ this.callback_success(result);
+ }).catch(error => {
+ if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */
+ this.finished_resolvers.push(resolver_name);
+
+ log.trace(LogCategory.DNS, tr(" Resolver %s ran into an error: %o"), resolver_name, error);
+ this.invoke_resolvers();
+ }).then(() => {
+ this.resolving_resolvers.remove(resolver_name);
+ if(!this.resolving_resolvers.length && this.resolving)
+ this.invoke_resolvers();
+ });
+ }
+
+ if(invoke_count === 0 && !this.resolving_resolvers.length && this.resolving) {
+ this.callback_fail("no response");
+ }
+ }
+}
+
+const kResolverLocalhost = new LocalhostResolver();
+
+const kResolverIpV4 = new IPResolveMethod(false);
+const kResolverIpV6 = new IPResolveMethod(true);
+
+const resolverSrvTS = new SRV_IPResolveMethod(new SRVResolveMethod("_ts._udp"), kResolverIpV4, kResolverIpV6);
+const resolverSrvTS3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp"), kResolverIpV4, kResolverIpV6);
+
+const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS);
+const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3);
+
+export async function resolveTeaSpeakServerAddress(address: ServerAddress, _options?: ResolveOptions) : Promise {
+ const options = Object.assign({}, default_options);
+ Object.assign(options, _options);
+
+ const resolver = new TeaSpeakDNSResolve(address);
+
+ resolver.registerResolver(kResolverLocalhost);
+
+ resolver.registerResolver(resolverSrvTS, kResolverLocalhost);
+ resolver.registerResolver(resolverSrvTS3, kResolverLocalhost);
+ //TODO: TSDNS somehow?
+
+ resolver.registerResolver(resolverDrSrvTS, resolverSrvTS);
+ resolver.registerResolver(resolverDrSrvTS3, resolverSrvTS3);
+
+ resolver.registerResolver(kResolverIpV4, resolverSrvTS, resolverSrvTS3);
+ resolver.registerResolver(kResolverIpV6, kResolverIpV4);
+
+ const response = await resolver.resolve(options.timeout || 5000);
+ return {
+ target_ip: response.host,
+ target_port: response.port
+ };
+}
+
+export async function resolveAddressIpv4(address: string) : Promise {
+ const result = await executeDnsRequest(address, RRType.A);
+ if(!result.length) return undefined;
+
+ return result[0].data;
+}
\ No newline at end of file
diff --git a/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts b/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts
index f586d4fd..167ec79b 100644
--- a/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts
+++ b/web/app/legacy/voice/bridge/NativeWebRTCVoiceBridge.ts
@@ -6,7 +6,7 @@ import {tr} from "tc-shared/i18n/localize";
import {WebRTCVoiceBridge} from "./WebRTCVoiceBridge";
import {VoiceWhisperPacket} from "./VoiceBridge";
import {CryptoHelper} from "tc-shared/profiles/identities/TeamSpeakIdentity";
-import arraybuffer_to_string = CryptoHelper.arraybuffer_to_string;
+import arraybuffer_to_string = CryptoHelper.arraybufferToString;
export class NativeWebRTCVoiceBridge extends WebRTCVoiceBridge {
static isSupported(): boolean {