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