ci
ci/woodpecker/push/base Pipeline failed Details

lanparty
gapodo 2023-11-19 23:00:03 +01:00
parent feb564d4a0
commit 3ec3db37e0
9 changed files with 322 additions and 126 deletions

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
audit=false
fund=false
update-notifier=false
package-lock=true

42
.woodpecker/base.yaml Normal file
View File

@ -0,0 +1,42 @@
when:
event: [push, deployment, manual, cron]
labels:
platform: linux/amd64
variables:
- &node_image 'node:14-bullseye'
- &buildx_image 'woodpeckerci/plugin-docker-buildx:2.2.1'
steps:
prepare-npm:
image: *node_image
secrets:
- npmconf
commands:
- git config --add safe.directory '*'
- if [ "$${NPMCONF:-}" != "" ]; then echo "$${NPMCONF}" >> "$${HOME}/.npmrc"; fi
- npm ci
- npx browserslist@latest --update-db
build-npm:
image: *node_image
commands:
- bash ./scripts/build.sh web rel
build-docker:
image: *buildx_image
pull: true
settings:
platforms: linux/amd64
dockerfile: docker/Dockerfile.ci
context: .
registry:
from_secret: registry_domain
tag: latest
repo:
from_secret: target_image_name
password:
from_secret: registry_token
username:
from_secret: registry_user

17
docker/Dockerfile.base Normal file
View File

@ -0,0 +1,17 @@
FROM nginx:mainline-alpine
COPY ./files/default.conf /etc/nginx/conf.d/default.conf
COPY ./files/nginx.conf /etc/nginx/nginx.conf
COPY ./files/entrypoint.sh /
RUN apk update --no-cache && apk upgrade --no-cache \
&& apk add --no-cache openssl tzdata \
&& mkdir -p /var/www/TeaWeb /etc/ssl/certs \
&& chmod +x /entrypoint.sh
ENV TZ="Europe/Berlin"
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

19
docker/Dockerfile.ci Normal file
View File

@ -0,0 +1,19 @@
FROM nginx:mainline-alpine
COPY ./docker/files/default.conf /etc/nginx/conf.d/default.conf
COPY ./docker/files/nginx.conf /etc/nginx/nginx.conf
COPY ./docker/files/entrypoint.sh /
RUN apk update --no-cache && apk upgrade --no-cache \
&& apk add --no-cache openssl tzdata \
&& mkdir -p /var/www/TeaWeb /etc/ssl/certs \
&& chmod +x /entrypoint.sh
ENV TZ="Europe/Berlin"
EXPOSE 80 443
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
COPY ./dist/ /var/www/TeaWeb/

36
docker/default.conf Normal file
View File

@ -0,0 +1,36 @@
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 default_server ssl http2;
server_name _;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:TLS-CHACHA20-POLY1305-SHA256:TLS-AES-256-GCM-SHA384:TLS-AES-128-GCM-SHA256;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_ecdh_curve secp384r1;
ssl_certificate /etc/ssl/certs/tea_bundle.crt;
ssl_certificate_key /etc/ssl/certs/tea.key;
ssl_session_cache shared:MozSSL:10m;
ssl_session_timeout 1d;
ssl_prefer_server_ciphers on;
location / {
root /var/www/TeaWeb;
index index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
gzip off;
}

29
docker/entrypoint.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env sh
set -e
gen_self_signed() {
echo "[WRN] No certificates found, generating self signed cert with key"
openssl req -x509 -nodes -days 1780 -newkey rsa:4096 \
-keyout /etc/ssl/certs/tea.key \
-out /etc/ssl/certs/tea_bundle.crt \
-subj "/C=DE/ST=Berlin/L=Germany/O=TeaSpeak/OU=TeaWeb/CN=localhost/emailAddress=noreply@teaspeak.de"
}
gen_diffie_hellman() {
echo "[INF] No Diffie-Hellman pem found, generating new with 2048 byte"
openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
}
if [ "$1" = "nginx" ]; then
if [ ! -f /etc/ssl/certs/tea.key ] && [ ! -f /etc/ssl/certs/tea_bundle.crt ]; then
gen_self_signed
elif [ ! -f /etc/ssl/certs/tea.key ] || [ ! -f /etc/ssl/certs/tea_bundle.crt ]; then
echo "[ERR] Only found a key or crt-bundle file but both files are REQUIRED!"
exit 1
fi
if [ ! -f /etc/ssl/certs/dhparam.pem ]; then
gen_diffie_hellman
fi
fi
exec "$@"

32
docker/nginx.conf Normal file
View File

@ -0,0 +1,32 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
server_tokens off;
keepalive_timeout 75;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -1,10 +1,10 @@
import {LogCategory, logError, logInfo, logTrace} from "./log"; import { LogCategory, logError, logInfo, logTrace } from "./log";
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {Stage} from "tc-loader"; import { Stage } from "tc-loader";
import {Registry} from "./events"; import { Registry } from "./events";
import {tr} from "./i18n/localize"; import { tr } from "./i18n/localize";
import {CallOnce, ignorePromise} from "tc-shared/proto"; import { CallOnce, ignorePromise } from "tc-shared/proto";
import {getStorageAdapter} from "tc-shared/StorageAdapter"; import { getStorageAdapter } from "tc-shared/StorageAdapter";
/* /*
* TODO: Sync settings across renderer instances * TODO: Sync settings across renderer instances
@ -14,17 +14,17 @@ export type RegistryValueType = boolean | number | string | object;
export type RegistryValueTypeNames = "boolean" | "number" | "string" | "object"; export type RegistryValueTypeNames = "boolean" | "number" | "string" | "object";
export type RegistryValueTypeMapping<T> = T extends boolean ? "boolean" : export type RegistryValueTypeMapping<T> = T extends boolean ? "boolean" :
T extends number ? "number" : T extends number ? "number" :
T extends string ? "string" : T extends string ? "string" :
T extends object ? "object" : T extends object ? "object" :
never; never;
export interface RegistryKey<ValueType extends RegistryValueType> { export interface RegistryKey<ValueType extends RegistryValueType> {
key: string; key: string;
valueType: RegistryValueTypeMapping<ValueType>; valueType: RegistryValueTypeMapping<ValueType>;
fallbackKeys?: string | string[]; fallbackKeys?: string | string[];
fallbackImports?: {[key: string]:(value: string) => ValueType}; fallbackImports?: { [key: string]: (value: string) => ValueType };
description?: string; description?: string;
@ -37,7 +37,7 @@ export interface ValuedRegistryKey<ValueType extends RegistryValueType> extends
const UPDATE_DIRECT: boolean = true; const UPDATE_DIRECT: boolean = true;
function decodeValueFromString<T extends RegistryValueType>(input: string, type: RegistryValueTypeMapping<T>) : T { function decodeValueFromString<T extends RegistryValueType>(input: string, type: RegistryValueTypeMapping<T>): T {
switch (type) { switch (type) {
case "string": case "string":
return input as any; return input as any;
@ -60,7 +60,7 @@ function decodeValueFromString<T extends RegistryValueType>(input: string, type:
} }
} }
export function encodeSettingValueToString<T extends RegistryValueType>(input: T) : string { export function encodeSettingValueToString<T extends RegistryValueType>(input: T): string {
switch (typeof input) { switch (typeof input) {
case "string": case "string":
return input; return input;
@ -83,24 +83,24 @@ export function resolveSettingKey<ValueType extends RegistryValueType, DefaultTy
key: RegistryKey<ValueType>, key: RegistryKey<ValueType>,
resolver: (key: string) => string | undefined | null, resolver: (key: string) => string | undefined | null,
defaultValue: DefaultType defaultValue: DefaultType
) : ValueType | DefaultType { ): ValueType | DefaultType {
let value = resolver(key.key); let value = resolver(key.key);
const keys = [key.key]; const keys = [key.key];
if(Array.isArray(key.fallbackKeys)) { if (Array.isArray(key.fallbackKeys)) {
keys.push(...key.fallbackKeys); keys.push(...key.fallbackKeys);
} }
for(const resolveKey of keys) { for (const resolveKey of keys) {
value = resolver(resolveKey); value = resolver(resolveKey);
if(typeof value !== "string") { if (typeof value !== "string") {
continue; continue;
} }
switch (key.valueType) { switch (key.valueType) {
case "number": case "number":
case "boolean": case "boolean":
if(value.length === 0) { if (value.length === 0) {
continue; continue;
} }
break; break;
@ -109,9 +109,9 @@ export function resolveSettingKey<ValueType extends RegistryValueType, DefaultTy
break; break;
} }
if(key.fallbackImports) { if (key.fallbackImports) {
const fallbackValueImporter = key.fallbackImports[resolveKey]; const fallbackValueImporter = key.fallbackImports[resolveKey];
if(fallbackValueImporter) { if (fallbackValueImporter) {
return fallbackValueImporter(value); return fallbackValueImporter(value);
} }
} }
@ -129,21 +129,21 @@ export class UrlParameterParser {
this.url = url; this.url = url;
} }
private getParameter(key: string) : string | undefined { private getParameter(key: string): string | undefined {
const value = this.url.searchParams.get(key); const value = this.url.searchParams.get(key);
if(value === null) { if (value === null) {
return undefined; return undefined;
} }
return decodeURIComponent(value); return decodeURIComponent(value);
} }
getValue<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV) : V | DV; getValue<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV): V | DV;
getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V) : V; getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V): V;
getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV) : V | DV { getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV): V | DV {
if(arguments.length > 1) { if (arguments.length > 1) {
return resolveSettingKey(key, key => this.getParameter(key), defaultValue); return resolveSettingKey(key, key => this.getParameter(key), defaultValue);
} else if("defaultValue" in key) { } else if ("defaultValue" in key) {
return resolveSettingKey(key, key => this.getParameter(key), key.defaultValue); return resolveSettingKey(key, key => this.getParameter(key), key.defaultValue);
} else { } else {
throw tr("missing value"); throw tr("missing value");
@ -155,14 +155,14 @@ export class UrlParameterBuilder {
private parameters = {}; private parameters = {};
setValue<V extends RegistryValueType>(key: RegistryKey<V>, value: V) { setValue<V extends RegistryValueType>(key: RegistryKey<V>, value: V) {
if(value === undefined) { if (value === undefined) {
delete this.parameters[key.key]; delete this.parameters[key.key];
} else { } else {
this.parameters[key.key] = encodeURIComponent(encodeSettingValueToString(value)); this.parameters[key.key] = encodeURIComponent(encodeSettingValueToString(value));
} }
} }
build() : string { build(): string {
return Object.keys(this.parameters).map(key => `${key}=${this.parameters[key]}`).join("&"); return Object.keys(this.parameters).map(key => `${key}=${this.parameters[key]}`).join("&");
} }
} }
@ -174,12 +174,12 @@ export class UrlParameterBuilder {
export namespace AppParameters { export namespace AppParameters {
export const Instance = new UrlParameterParser(new URL(window.location.href)); export const Instance = new UrlParameterParser(new URL(window.location.href));
export function getValue<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV) : V | DV; export function getValue<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV): V | DV;
export function getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V) : V; export function getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V): V;
export function getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV) : V | DV { export function getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV): V | DV {
if(arguments.length > 1) { if (arguments.length > 1) {
return Instance.getValue(key, defaultValue); return Instance.getValue(key, defaultValue);
} else if("defaultValue" in key) { } else if ("defaultValue" in key) {
return Instance.getValue(key); return Instance.getValue(key);
} else { } else {
throw tr("missing value"); throw tr("missing value");
@ -408,12 +408,12 @@ export class Settings {
static readonly KEY_FLAG_CONNECT_DEFAULT: ValuedRegistryKey<boolean> = { static readonly KEY_FLAG_CONNECT_DEFAULT: ValuedRegistryKey<boolean> = {
key: "connect_default", key: "connect_default",
valueType: "boolean", valueType: "boolean",
defaultValue: false defaultValue: true
}; };
static readonly KEY_CONNECT_ADDRESS: ValuedRegistryKey<string> = { static readonly KEY_CONNECT_ADDRESS: ValuedRegistryKey<string> = {
key: "connect_address", key: "connect_address",
valueType: "string", valueType: "string",
defaultValue: undefined defaultValue: "tea.lp.kle.li"
}; };
static readonly KEY_CONNECT_PROFILE: ValuedRegistryKey<string> = { static readonly KEY_CONNECT_PROFILE: ValuedRegistryKey<string> = {
key: "connect_profile", key: "connect_profile",
@ -448,7 +448,7 @@ export class Settings {
static readonly KEY_CONNECT_NO_DNSPROXY: ValuedRegistryKey<boolean> = { static readonly KEY_CONNECT_NO_DNSPROXY: ValuedRegistryKey<boolean> = {
key: "connect_no_dnsproxy", key: "connect_no_dnsproxy",
defaultValue: false, defaultValue: true,
valueType: "boolean", valueType: "boolean",
}; };
@ -616,7 +616,7 @@ export class Settings {
/* defaultValue: <users download directory> */ /* defaultValue: <users download directory> */
}; };
static readonly KEY_IPC_REMOTE_ADDRESS: RegistryKey<string> = { static readonly KEY_IPC_REMOTE_ADDRESS: RegistryKey<string> = {
key: "ipc-address", key: "ipc-address",
valueType: "string" valueType: "string"
}; };
@ -913,8 +913,8 @@ export class Settings {
static readonly KEYS = (() => { static readonly KEYS = (() => {
const result = []; const result = [];
for(const key of Object.keys(Settings)) { for (const key of Object.keys(Settings)) {
if(!key.toUpperCase().startsWith("KEY_")) { if (!key.toUpperCase().startsWith("KEY_")) {
continue; continue;
} }
@ -943,13 +943,13 @@ export class Settings {
const json = await getStorageAdapter().get("settings.global"); const json = await getStorageAdapter().get("settings.global");
try { try {
if(json === null) { if (json === null) {
logInfo(LogCategory.GENERAL, tr("Found no settings. Creating new client settings.")); logInfo(LogCategory.GENERAL, tr("Found no settings. Creating new client settings."));
this.settingsCache = {}; this.settingsCache = {};
} else { } else {
this.settingsCache = JSON.parse(json); this.settingsCache = JSON.parse(json);
} }
} catch(error) { } catch (error) {
this.settingsCache = {}; this.settingsCache = {};
logError(LogCategory.GENERAL, tr("Failed to load global settings!\nJson: %s\nError: %o"), json, error); logError(LogCategory.GENERAL, tr("Failed to load global settings!\nJson: %s\nError: %o"), json, error);
@ -957,7 +957,7 @@ export class Settings {
//FIXME: Readd this //FIXME: Readd this
//createErrorModal(tr("Failed to load global settings"), tr("Failed to load global client settings!\nLookup console for more information.")).open(); //createErrorModal(tr("Failed to load global settings"), tr("Failed to load global client settings!\nLookup console for more information.")).open();
}; };
if(!loader.finished()) { if (!loader.finished()) {
loader.register_task(loader.Stage.LOADED, { loader.register_task(loader.Stage.LOADED, {
priority: 0, priority: 0,
name: "Settings error", name: "Settings error",
@ -969,18 +969,18 @@ export class Settings {
} }
this.saveWorker = setInterval(() => { this.saveWorker = setInterval(() => {
if(this.updated) { if (this.updated) {
this.save(); this.save();
} }
}, 5 * 1000); }, 5 * 1000);
} }
getValue<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV) : V | DV; getValue<V extends RegistryValueType, DV>(key: RegistryKey<V>, defaultValue: DV): V | DV;
getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V) : V; getValue<V extends RegistryValueType>(key: ValuedRegistryKey<V>, defaultValue?: V): V;
getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV) : V | DV { getValue<V extends RegistryValueType, DV>(key: RegistryKey<V> | ValuedRegistryKey<V>, defaultValue: DV): V | DV {
if(arguments.length > 1) { if (arguments.length > 1) {
return resolveSettingKey(key, key => this.settingsCache[key], defaultValue); return resolveSettingKey(key, key => this.settingsCache[key], defaultValue);
} else if("defaultValue" in key) { } else if ("defaultValue" in key) {
return resolveSettingKey(key, key => this.settingsCache[key], key.defaultValue); return resolveSettingKey(key, key => this.settingsCache[key], key.defaultValue);
} else { } else {
debugger; debugger;
@ -988,17 +988,17 @@ export class Settings {
} }
} }
setValue<T extends RegistryValueType>(key: RegistryKey<T>, value?: T){ setValue<T extends RegistryValueType>(key: RegistryKey<T>, value?: T) {
if(value === null) { if (value === null) {
value = undefined; value = undefined;
} }
if(this.settingsCache[key.key] === value) { if (this.settingsCache[key.key] === value) {
return; return;
} }
const oldValue = this.settingsCache[key.key]; const oldValue = this.settingsCache[key.key];
if(value === undefined) { if (value === undefined) {
delete this.settingsCache[key.key]; delete this.settingsCache[key.key];
} else { } else {
this.settingsCache[key.key] = encodeSettingValueToString(value); this.settingsCache[key.key] = encodeSettingValueToString(value);
@ -1014,21 +1014,21 @@ export class Settings {
}); });
logTrace(LogCategory.GENERAL, tr("Changing global setting %s to %o"), key.key, value); logTrace(LogCategory.GENERAL, tr("Changing global setting %s to %o"), key.key, value);
if(UPDATE_DIRECT) { if (UPDATE_DIRECT) {
this.save(); this.save();
} }
} }
globalChangeListener<T extends RegistryValueType>(key: RegistryKey<T>, listener: (newValue: T) => void) : () => void { globalChangeListener<T extends RegistryValueType>(key: RegistryKey<T>, listener: (newValue: T) => void): () => void {
return this.events.on("notify_setting_changed", event => { return this.events.on("notify_setting_changed", event => {
if(event.setting === key.key && event.mode === "global") { if (event.setting === key.key && event.mode === "global") {
listener(event.newCastedValue); listener(event.newCastedValue);
} }
}) })
} }
private async doSave() { private async doSave() {
if(this.saveState === "none") { if (this.saveState === "none") {
return; return;
} }
@ -1040,7 +1040,7 @@ export class Settings {
} catch (error) { } catch (error) {
logError(LogCategory.GENERAL, tr("Failed to save global settings: %o"), error); logError(LogCategory.GENERAL, tr("Failed to save global settings: %o"), error);
} }
} while(this.saveState !== "saving"); } while (this.saveState !== "saving");
this.saveState = "none"; this.saveState = "none";
} }

View File

@ -1,11 +1,11 @@
import {LogCategory, logError, logTrace, logWarn} from "tc-shared/log"; import { LogCategory, logError, logTrace, logWarn } from "tc-shared/log";
import {tr} from "tc-shared/i18n/localize"; import { tr } from "tc-shared/i18n/localize";
import {default_options, DNSAddress, DNSResolveResult, ResolveOptions} from "tc-shared/dns"; import { default_options, DNSAddress, DNSResolveResult, ResolveOptions } from "tc-shared/dns";
import {executeDnsRequest, RRType} from "./api"; import { executeDnsRequest, RRType } from "./api";
interface DNSResolveMethod { interface DNSResolveMethod {
name() : string; name(): string;
resolve(address: DNSAddress) : Promise<DNSAddress | undefined>; resolve(address: DNSAddress): Promise<DNSAddress | undefined>;
} }
class LocalhostResolver implements DNSResolveMethod { class LocalhostResolver implements DNSResolveMethod {
@ -14,7 +14,7 @@ class LocalhostResolver implements DNSResolveMethod {
} }
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
if(address.hostname === "localhost") { if (address.hostname === "localhost") {
return { return {
hostname: "127.0.0.1", hostname: "127.0.0.1",
port: address.port port: address.port
@ -26,6 +26,20 @@ class LocalhostResolver implements DNSResolveMethod {
} }
class FakeResolver implements DNSResolveMethod {
name(): string {
return "fake resolver";
}
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
return {
hostname: "tea.lp.kle.li",
port: address.port
}
}
}
class IPResolveMethod implements DNSResolveMethod { class IPResolveMethod implements DNSResolveMethod {
readonly v6: boolean; readonly v6: boolean;
@ -40,7 +54,7 @@ class IPResolveMethod implements DNSResolveMethod {
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const answer = await executeDnsRequest(address.hostname, this.v6 ? RRType.AAAA : RRType.A); const answer = await executeDnsRequest(address.hostname, this.v6 ? RRType.AAAA : RRType.A);
if(!answer.length) { if (!answer.length) {
return undefined; return undefined;
} }
@ -72,10 +86,10 @@ class SRVResolveMethod implements DNSResolveMethod {
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.hostname, RRType.SRV); const answer = await executeDnsRequest((this.application ? this.application + "." : "") + address.hostname, RRType.SRV);
const records: {[key: number]: ParsedSVRRecord[]} = {}; const records: { [key: number]: ParsedSVRRecord[] } = {};
for(const record of answer) { for (const record of answer) {
const parts = record.data.split(" "); const parts = record.data.split(" ");
if(parts.length !== 4) { if (parts.length !== 4) {
logWarn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record); logWarn(LogCategory.DNS, tr("Failed to parse SRV record %s. Invalid split length."), record);
continue; continue;
} }
@ -84,7 +98,7 @@ class SRVResolveMethod implements DNSResolveMethod {
const weight = parseInt(parts[1]); const weight = parseInt(parts[1]);
const port = parseInt(parts[2]); const port = parseInt(parts[2]);
if((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) { if ((priority < 0 || priority > 65535) || (weight < 0 || weight > 65535) || (port < 0 || port > 65535)) {
logWarn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record); logWarn(LogCategory.DNS, tr("Failed to parse SRV record %s. Malformed data."), record);
continue; continue;
} }
@ -99,35 +113,35 @@ class SRVResolveMethod implements DNSResolveMethod {
/* get the record with the highest priority */ /* get the record with the highest priority */
const priority_strings = Object.keys(records); const priority_strings = Object.keys(records);
if(!priority_strings.length) { if (!priority_strings.length) {
return undefined; return undefined;
} }
let highestPriority: ParsedSVRRecord[]; let highestPriority: ParsedSVRRecord[];
for(const priority_str of priority_strings) { for (const priority_str of priority_strings) {
if(!highestPriority || !highestPriority.length) { if (!highestPriority || !highestPriority.length) {
highestPriority = records[priority_str]; highestPriority = records[priority_str];
} }
if(highestPriority[0].priority < parseInt(priority_str)) { if (highestPriority[0].priority < parseInt(priority_str)) {
highestPriority = records[priority_str]; highestPriority = records[priority_str];
} }
} }
if(!highestPriority.length) { if (!highestPriority.length) {
return undefined; return undefined;
} }
/* select randomly one record */ /* select randomly one record */
let record: ParsedSVRRecord; let record: ParsedSVRRecord;
const max_weight = highestPriority.map(e => e.weight).reduce((a, b) => a + b, 0); const max_weight = highestPriority.map(e => e.weight).reduce((a, b) => a + b, 0);
if(max_weight == 0) { if (max_weight == 0) {
record = highestPriority[Math.floor(Math.random() * highestPriority.length)]; record = highestPriority[Math.floor(Math.random() * highestPriority.length)];
} else { } else {
let rnd = Math.random() * max_weight; let rnd = Math.random() * max_weight;
for(let i = 0; i < highestPriority.length; i++) { for (let i = 0; i < highestPriority.length; i++) {
rnd -= highestPriority[i].weight; rnd -= highestPriority[i].weight;
if(rnd > 0) { if (rnd > 0) {
continue; continue;
} }
@ -136,7 +150,7 @@ class SRVResolveMethod implements DNSResolveMethod {
} }
} }
if(!record) { if (!record) {
/* shall never happen */ /* shall never happen */
record = highestPriority[0]; record = highestPriority[0];
} }
@ -165,7 +179,7 @@ class SRV_IPResolveMethod implements DNSResolveMethod {
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const srvAddress = await this.srvResolver.resolve(address); const srvAddress = await this.srvResolver.resolve(address);
if(!srvAddress) { if (!srvAddress) {
return undefined; return undefined;
} }
@ -190,7 +204,7 @@ class DomainRootResolveMethod implements DNSResolveMethod {
async resolve(address: DNSAddress): Promise<DNSAddress | undefined> { async resolve(address: DNSAddress): Promise<DNSAddress | undefined> {
const parts = address.hostname.split("."); const parts = address.hostname.split(".");
if(parts.length < 3) { if (parts.length < 3) {
return undefined; return undefined;
} }
@ -203,7 +217,7 @@ class DomainRootResolveMethod implements DNSResolveMethod {
class TeaSpeakDNSResolve { class TeaSpeakDNSResolve {
readonly address: DNSAddress; readonly address: DNSAddress;
private resolvers: {[key: string]:{ resolver: DNSResolveMethod, after: string[] }} = {}; private resolvers: { [key: string]: { resolver: DNSResolveMethod, after: string[] } } = {};
private resolving = false; private resolving = false;
private timeout; private timeout;
@ -218,15 +232,15 @@ class TeaSpeakDNSResolve {
} }
registerResolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) { registerResolver(resolver: DNSResolveMethod, ...after: (string | DNSResolveMethod)[]) {
if(this.resolving) { if (this.resolving) {
throw tr("resolver is already resolving"); throw tr("resolver is already resolving");
} }
this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) }; this.resolvers[resolver.name()] = { resolver: resolver, after: after.map(e => typeof e === "string" ? e : e.name()) };
} }
resolve(timeout: number) : Promise<DNSAddress> { resolve(timeout: number): Promise<DNSAddress> {
if(this.resolving) { if (this.resolving) {
throw tr("already resolving"); throw tr("already resolving");
} }
this.resolving = true; this.resolving = true;
@ -263,52 +277,53 @@ class TeaSpeakDNSResolve {
let invoke_count = 0; let invoke_count = 0;
_main_loop: _main_loop:
for(const resolver_name of Object.keys(this.resolvers)) { for (const resolver_name of Object.keys(this.resolvers)) {
if(this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue; if (this.resolving_resolvers.findIndex(e => e === resolver_name) !== -1) continue;
if(this.finished_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]; const resolver = this.resolvers[resolver_name];
for(const after of resolver.after) for (const after of resolver.after)
if(this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop; if (this.finished_resolvers.findIndex(e => e === after) === -1) continue _main_loop;
invoke_count++; invoke_count++;
logTrace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name); logTrace(LogCategory.DNS, tr(" Executing resolver %s"), resolver_name);
this.resolving_resolvers.push(resolver_name); this.resolving_resolvers.push(resolver_name);
resolver.resolver.resolve(this.address).then(result => { resolver.resolver.resolve(this.address).then(result => {
if(!this.resolving || !this.callback_success) return; /* resolve has been finished already */ if (!this.resolving || !this.callback_success) return; /* resolve has been finished already */
this.finished_resolvers.push(resolver_name); this.finished_resolvers.push(resolver_name);
if(!result) { if (!result) {
logTrace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name); logTrace(LogCategory.DNS, tr(" Resolver %s returned an empty response."), resolver_name);
this.invoke_resolvers();
return;
}
logTrace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"),
this.address.hostname, this.address.port,
result.hostname, 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);
logTrace(LogCategory.DNS, tr(" Resolver %s ran into an error: %o"), resolver_name, error);
this.invoke_resolvers(); this.invoke_resolvers();
}).then(() => { return;
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) { logTrace(LogCategory.DNS, tr(" Successfully resolved address %s:%d to %s:%d via resolver %s"),
this.address.hostname, this.address.port,
result.hostname, 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);
logTrace(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"); this.callback_fail("no response");
} }
} }
} }
const kResolverFake = new FakeResolver();
const kResolverLocalhost = new LocalhostResolver(); const kResolverLocalhost = new LocalhostResolver();
const kResolverIpV4 = new IPResolveMethod(false); const kResolverIpV4 = new IPResolveMethod(false);
@ -320,14 +335,16 @@ const resolverSrvTS3 = new SRV_IPResolveMethod(new SRVResolveMethod("_ts3._udp")
const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS); const resolverDrSrvTS = new DomainRootResolveMethod(resolverSrvTS);
const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3); const resolverDrSrvTS3 = new DomainRootResolveMethod(resolverSrvTS3);
export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options?: ResolveOptions) : Promise<DNSResolveResult> { export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options?: ResolveOptions): Promise<DNSResolveResult> {
try { try {
const options = Object.assign({}, default_options); const options = Object.assign({}, default_options);
Object.assign(options, _options); Object.assign(options, _options);
const resolver = new TeaSpeakDNSResolve(address); const resolver = new TeaSpeakDNSResolve(address);
resolver.registerResolver(kResolverLocalhost); resolver.registerResolver(kResolverFake);
resolver.registerResolver(kResolverLocalhost, kResolverFake);
resolver.registerResolver(resolverSrvTS, kResolverLocalhost); resolver.registerResolver(resolverSrvTS, kResolverLocalhost);
resolver.registerResolver(resolverSrvTS3, kResolverLocalhost); resolver.registerResolver(resolverSrvTS3, kResolverLocalhost);
@ -340,7 +357,7 @@ export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options
resolver.registerResolver(kResolverIpV6, kResolverIpV4); resolver.registerResolver(kResolverIpV6, kResolverIpV4);
const response = await resolver.resolve(options.timeout || 5000); const response = await resolver.resolve(options.timeout || 5000);
if(!response) { if (!response) {
return { return {
status: "empty-result" status: "empty-result"
}; };
@ -352,7 +369,7 @@ export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options
resolvedAddress: response resolvedAddress: response
}; };
} catch (error) { } catch (error) {
if(typeof error !== "string") { if (typeof error !== "string") {
logError(LogCategory.DNS, tr("Failed to resolve %o: %o"), address, error); logError(LogCategory.DNS, tr("Failed to resolve %o: %o"), address, error);
error = tr("lookup the console"); error = tr("lookup the console");
} }
@ -364,9 +381,9 @@ export async function resolveTeaSpeakServerAddress(address: DNSAddress, _options
} }
} }
export async function resolveAddressIpV4(address: string) : Promise<string> { export async function resolveAddressIpV4(address: string): Promise<string> {
const result = await executeDnsRequest(address, RRType.A); const result = await executeDnsRequest(address, RRType.A);
if(!result.length) return undefined; if (!result.length) return undefined;
return result[0].data; return result[0].data;
} }