Commit f917e7d9 authored by nanahira's avatar nanahira

support multiple sources

parent 1d7f0337
Pipeline #16314 passed with stages
in 3 minutes and 8 seconds
......@@ -3,20 +3,23 @@ import axios from 'axios';
import _ from 'lodash';
import YAML from 'yaml';
import fs from 'fs';
import { CronJob } from 'cron';
import net from 'net';
import { assert } from 'console';
import { ping } from 'icmp';
import delay from 'delay';
interface RecordRule {
match?: string;
interface Source {
protocol: string;
port: number;
testDomains?: string[];
source?: string;
}
interface RecordRule {
match?: string;
sources: Source[];
}
interface Config {
aliyun: Aliyun.Config;
domain: string;
......@@ -58,8 +61,7 @@ let config: Config;
interface DomainRecordInfo {
recordRule: RecordRule;
record: DomainRecord;
protocol: string;
port: number;
sources: Source[];
isCDN: boolean;
good: ConnectResult;
}
......@@ -84,7 +86,7 @@ class Checker {
private CDNRecords: DomainRecordInfo[];
private checkMethods = new Map<
string,
(record: RecordRule, address: string) => Promise<ConnectResult>
(record: Source, address: string) => Promise<ConnectResult>
>();
private availableRecordRules: RecordRule[];
......@@ -92,53 +94,96 @@ class Checker {
this.availableRecordRules = [];
await Promise.all(
this.config.cdnRecords.map(async (rule) => {
if (rule.protocol === 'tcp' && rule.source) {
this.message(`Checking source ${rule.source}:${rule.port}.`);
try {
const ms = await this.checkTcpProcess(rule.source, rule.port);
this.availableRecordRules.push(rule);
this.message(
`Source ${rule.source}:${rule.port} is good: ${ms} ms.`,
);
} catch (e) {
this.message(
`Skipping rule ${rule.match} fo source ${rule.source}:${
rule.port
} unhealthy: ${e.toString()}`,
);
}
} else if (
rule.source &&
(rule.protocol === 'http' || rule.protocol === 'https')
) {
const availableTestDomains: string[] = [];
for (const domain of rule.testDomains) {
this.message(`Checking source domain ${domain}:${rule.port}.`);
const errMessage = await this.tryConnectHttp(
rule.protocol,
domain,
rule.port,
domain,
);
if (errMessage) {
const { sources } = rule;
const goodSources: Source[] = [];
await Promise.all(
sources.map(async (source) => {
if (source.protocol === 'tcp' && source.source) {
this.message(
`Skipping domain ${domain} of rule ${rule.match} for bad source: ${errMessage}`,
`Checking source ${this.getSourcePattern(
source,
source.source,
)}.`,
);
try {
const ms = await this.checkTcpProcess(
source.source,
source.port,
);
goodSources.push(source);
this.message(
`Source ${this.getSourcePattern(
source,
source.source,
)} is good: ${ms} ms.`,
);
} catch (e) {
this.message(
`Skipping rule ${
rule.match
} for source ${this.getSourcePattern(
source,
source.source,
)} unhealthy: ${e.toString()}`,
);
}
} else if (
source.protocol === 'http' ||
source.protocol === 'https'
) {
const availableTestDomains: string[] = [];
await Promise.all(
source.testDomains.map(async (domain) => {
this.message(
`Checking source domain ${this.getSourcePattern(
source,
domain,
)}.`,
);
const errMessage = await this.tryConnectHttp(
source.protocol,
domain,
source.port,
domain,
);
if (errMessage) {
this.message(
`Skipping domain ${this.getSourcePattern(
source,
domain,
)} of rule ${rule.match} for bad source: ${errMessage}`,
);
} else {
this.message(
`Source domain ${this.getSourcePattern(
source,
domain,
)} is good.`,
);
availableTestDomains.push(domain);
}
}),
);
if (availableTestDomains.length) {
source.testDomains = availableTestDomains;
goodSources.push(source);
} else {
this.message(
`Skipping source ${rule.match} ${source.testDomains.join(
',',
)} for no available source domains.`,
);
}
} else {
this.message(`Source domain ${domain} is good.`);
availableTestDomains.push(domain);
goodSources.push(source);
}
}
if (availableTestDomains.length) {
rule.testDomains = availableTestDomains;
this.availableRecordRules.push(rule);
} else {
this.message(
`Skipping rule ${rule.match} for no available sources.`,
);
}
} else {
}),
);
if (goodSources.length) {
rule.sources = goodSources;
this.availableRecordRules.push(rule);
} else {
this.message(`Skipping rule ${rule.match} for no available sources.`);
}
}),
);
......@@ -167,7 +212,7 @@ class Checker {
// const urlKey = `${url}-${hostHeader}`;
return this.connectHttpProcess(url, hostHeader);
}
async checkHttpOrHttps(record: RecordRule, address: string) {
async checkHttpOrHttps(record: Source, address: string) {
let good = false;
if (!record.testDomains.length) {
return ConnectResult.SourceBad;
......@@ -194,7 +239,10 @@ class Checker {
);
if (nodeErrorMessage != null) {
this.message(
`Node ${address}:${record.port} is broken: ${nodeErrorMessage}`,
`Connection ${this.getSourcePattern(
record,
address,
)} is broken: ${nodeErrorMessage}`,
);
return ConnectResult.CDNBad;
} else {
......@@ -202,10 +250,17 @@ class Checker {
}
}
if (!good) {
this.message(`Node ${address}:${record.port} skipped for no sources.`);
this.message(
`Connection ${this.getSourcePattern(
record,
address,
)} skipped for no sources.`,
);
return ConnectResult.SourceBad;
}
this.message(`Node ${address}:${record.port} is good.`);
this.message(
`Connection ${this.getSourcePattern(record, address)} is good.`,
);
return ConnectResult.Good;
}
async checkTcpProcess(address: string, port: number) {
......@@ -238,7 +293,7 @@ class Checker {
}
});
}
async checkTcp(record: RecordRule, address: string) {
async checkTcp(record: Source, address: string) {
/*if (record.source) {
try {
await this.checkTcpProcess(record.source, record.port);
......@@ -251,20 +306,37 @@ class Checker {
}*/
try {
const ms = await this.checkTcpProcess(address, record.port);
this.message(`Node ${address}:${record.port} is good: ${ms} ms.`);
this.message(
`Connection ${this.getSourcePattern(
record,
address,
)} is good: ${ms} ms.`,
);
return ConnectResult.Good;
} catch (e) {
this.message(`Node ${address}:${record.port} failed: ${e.toString()}`);
this.message(
`Connection ${this.getSourcePattern(
record,
address,
)} failed: ${e.toString()}`,
);
return ConnectResult.CDNBad;
}
}
async checkIcmp(address: string) {
async checkIcmp(record: Source, address: string) {
try {
await ping(address, this.config.timeout);
this.message(`Node ${address}:ICMP is good.`);
this.message(
`Connection ${this.getSourcePattern(record, address)} is good.`,
);
return ConnectResult.Good;
} catch (e) {
this.message(`Node ${address}:ICMP failed: ${e.toString()}`);
this.message(
`Connection ${this.getSourcePattern(
record,
address,
)} failed: ${e.toString()}`,
);
return ConnectResult.CDNBad;
}
}
......@@ -272,17 +344,17 @@ class Checker {
this.client = new Aliyun(config.aliyun);
this.cdnRecordsRegex = config.cdnRecords.map((m) => new RegExp(m.match));
this.id = ++Checker.order;
this.checkMethods.set('http', (record: RecordRule, address: string) => {
this.checkMethods.set('http', (record: Source, address: string) => {
return this.checkHttpOrHttps(record, address);
});
this.checkMethods.set('https', (record: RecordRule, address: string) => {
this.checkMethods.set('https', (record: Source, address: string) => {
return this.checkHttpOrHttps(record, address);
});
this.checkMethods.set('tcp', (record: RecordRule, address: string) => {
this.checkMethods.set('tcp', (record: Source, address: string) => {
return this.checkTcp(record, address);
});
this.checkMethods.set('icmp', (record: RecordRule, address: string) => {
return this.checkIcmp(address);
this.checkMethods.set('icmp', (record: Source, address: string) => {
return this.checkIcmp(record, address);
});
}
private message(msg: string) {
......@@ -300,9 +372,14 @@ class Checker {
valuePrefix && this.cdnRecordsRegex.some((r) => !!valuePrefix.match(r))
);
}
getSourcePattern(source: Source, address: string) {
return `${source.protocol}://${address}:${source.port}`;
}
getRecordPattern(recordInfo: DomainRecordInfo) {
const record = recordInfo.record;
return `${record.RR}.${this.config.domain} => ${record.Value}:${recordInfo.port}`;
return `${record.RR}.${this.config.domain} => ${recordInfo.sources
.map((s) => this.getSourcePattern(s, record.Value))
.join(', ')}`;
}
async getRecords(): Promise<DomainRecordInfo[]> {
this.message(`Fetching domain records of ${this.config.domain}.`);
......@@ -338,13 +415,11 @@ class Checker {
);
continue;
}
const { port, protocol } = matchCDNRecord;
const isCDN = this.isCDNRecord(record);
const recordInfo: DomainRecordInfo = {
recordRule: matchCDNRecord,
record,
protocol,
port,
sources: matchCDNRecord.sources,
isCDN,
good: null,
};
......@@ -354,27 +429,58 @@ class Checker {
}
return res;
}
async checkNode(record: RecordRule, address: string): Promise<ConnectResult> {
const checkMethodFunction = this.checkMethods.get(record.protocol);
async checkSource(source: Source, address: string): Promise<ConnectResult> {
const checkMethodFunction = this.checkMethods.get(source.protocol);
assert(
checkMethodFunction,
`Check method ${record.protocol} not supported.`,
`Check method ${source.protocol} not supported.`,
);
let lastResult: ConnectResult;
for (let i = 1; i <= this.config.retryCount; ++i) {
const result = await checkMethodFunction(record, address);
const result = await checkMethodFunction(source, address);
if (result == ConnectResult.Good) {
return result;
}
lastResult = result;
}
if (lastResult === ConnectResult.CDNBad) {
this.message(`Node ${address}:${record.port} is bad.`);
this.message(
`Connection ${this.getSourcePattern(source, address)} is bad.`,
);
} else {
this.message(`Node ${address}:${record.port} skipped for source broken.`);
this.message(
`Connection ${this.getSourcePattern(
source,
address,
)} skipped for source broken.`,
);
}
return lastResult;
}
async checkNode(recordInfo: DomainRecordInfo): Promise<ConnectResult> {
const record = recordInfo.recordRule;
const address = recordInfo.record.Value;
const checks = await Promise.all(
record.sources.map(async (source) => this.checkSource(source, address)),
);
if (checks.some((r) => r === ConnectResult.CDNBad)) {
this.message(`Node ${this.getRecordPattern(recordInfo)} is bad.`);
return ConnectResult.CDNBad;
}
if (checks.every((r) => r === ConnectResult.SourceBad)) {
this.message(
`Node ${this.getRecordPattern(
recordInfo,
)} skipped for no good sources.`,
);
return ConnectResult.SourceBad;
}
this.message(`Node ${this.getRecordPattern(recordInfo)} is good.`);
return ConnectResult.Good;
}
async checkRecord(recordInfo: DomainRecordInfo) {
const record = recordInfo.record;
this.message(
......@@ -382,7 +488,7 @@ class Checker {
recordInfo,
)} with old status of ${record.Status}.`,
);
const good = await this.checkNode(recordInfo.recordRule, record.Value);
const good = await this.checkNode(recordInfo);
await this.handleRecordResult(recordInfo, good);
}
isRecordGood(recordInfo: DomainRecordInfo): boolean {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment