Commit f70c57c6 authored by nanahira's avatar nanahira

rework

parent 75eee2c2
Pipeline #1826 passed with stages
in 6 minutes and 25 seconds
FROM node:buster-slim
RUN apt update && apt -y install python3 && rm -rf /var/lib/apt/lists/*
RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src/app
COPY ./package*.json ./
RUN npm ci
......
......@@ -8,6 +8,8 @@ Build a Docker image from the given Dockerfile and run the container with `confi
A prebuilt docker image could be found at `nanahira/cdn-node-checker` at DockerHub or `git-registry.mycard.moe/nanahira/cdn-node-checker` at MyCard Git.
Privileged container is needed for `icmp` method.
**Important: Make sure to use public DNS in the container.** You may want to use the `dns: 114.114.114.114` option in `docker-compose.yml` file.
## Config example
......@@ -21,12 +23,14 @@ aliyun:
domain: yuzurisa.com
cdnRecords: # You may add multiple.
- match: '^cdn-[-a-zA-Z]+$' # The matching domain records for CDN.
protocol: https # Currently, supported values are `icmp`, `tcp`, `http` and `https`
port: 443 # Change this if you are using non-standard ports.
testDomains:
- ygobbs.com # Testing sources.
- nanahira.momobako.com
timeout: 10000
retryCount: 3
cronString: "0 * * * * *"
interval: 120000
```
......@@ -6,10 +6,11 @@ aliyun:
domain: yuzurisa.com
cdnRecords: # You may add multiple.
- match: '^cdn-[-a-zA-Z]+$' # The matching domain records for CDN.
protocol: https # Currently, supported values are `icmp`, `tcp`, `http` and `https`
port: 443 # Change this if you are using non-standard ports.
testDomains:
- ygobbs.com # Testing sources.
- nanahira.momobako.com
timeout: 10000
retryCount: 3
cronString: "0 * * * * *"
interval: 120000
......@@ -30,6 +30,11 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.14.tgz",
"integrity": "sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ=="
},
"@types/q": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"@types/underscore": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.3.tgz",
......@@ -56,14 +61,6 @@
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.1.0.tgz",
"integrity": "sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA=="
},
"cron": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz",
"integrity": "sha512-Gk2c4y6xKEO8FSAUTklqtfSr7oTq0CiPQeLBG5Fl0qoXpZyMcj1SG59YL+hqq04bu6/IuEA7lMkYDAplQNKkyg==",
"requires": {
"moment-timezone": "^0.5.x"
}
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
......@@ -119,6 +116,14 @@
}
}
},
"icmp": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/icmp/-/icmp-1.4.0.tgz",
"integrity": "sha512-5V1e9D4v8YnOp1mUDZFHjbubip6J2bXvpk0u9KlxSc0up8Xn94CImRUvXOWvVmYxUvTMoVIRkSZhqzkl6fKcKA==",
"requires": {
"raw-socket": "^1.7.0"
}
},
"json-bigint": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.2.3.tgz",
......@@ -137,19 +142,29 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"moment-timezone": {
"version": "0.5.31",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
"integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
"requires": {
"moment": ">= 2.9.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nan": {
"version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
"raw-socket": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/raw-socket/-/raw-socket-1.7.0.tgz",
"integrity": "sha512-mXqWihgwaFNmV5le0dWk5o+03M3A2zBIkC9BNaE6R0CJN9eYot++j2FIqgNSDq6/Vmu32PPI155SiiWNV2yyFQ==",
"requires": {
"nan": "2.14.*"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
......
import Aliyun from "@alicloud/pop-core";
import axios from "axios";
import _ from "underscore";
import _, { delay } from "underscore";
import YAML from "yaml";
import fs from "fs";
import {CronJob} from "cron";
import { CronJob } from "cron";
import net from "net";
import { assert } from "console";
import Q from "q";
import { ping } from "icmp";
interface CDNRecord {
match: string;
protocol: string;
port: number;
}
......@@ -17,7 +22,7 @@ interface Config {
testDomains: string[];
timeout: number;
retryCount: number;
cronString: string;
interval: number;
}
interface DomainRecordObject {
......@@ -51,6 +56,7 @@ let config: Config;
interface DomainRecordInfo {
record: DomainRecord;
protocol: string;
port: number;
isCDN: boolean;
good: boolean;
......@@ -69,11 +75,95 @@ class Checker {
records: DomainRecordInfo[];
nonCDNRecords: DomainRecordInfo[];
CDNRecords: DomainRecordInfo[];
checkMethods: Map<string, (protocol: string, address: string, port: number) => Promise<boolean>>;
async checkHttpOrHttps(protocol: string, address: string, port: number) {
let currentTestDomain: string;
try {
for (let testDomain of this.config.testDomains) {
currentTestDomain = testDomain;
await axios.get(`${protocol}://${address}:${port}`, {
headers: {
Host: testDomain
},
timeout: this.config.timeout,
validateStatus: status => status < 500
});
}
this.message(`Node ${address}:${port} is good.`);
return true;
} catch (e) {
this.message(`Node ${address}:${port} failed in checking ${currentTestDomain}: ${e.toString()}`);
return false;
}
}
async checkTcpProcess(address: string, port: number) {
return new Promise<number>((resolve, reject) => {
const hrstart = process.hrtime();
const socket = new net.Socket();
socket.connect(port, address);
socket.setTimeout(this.config.timeout || 1000);
socket.on('connect', () => {
socket.destroy();
resolve(milliseconds());
});
socket.on('error', (error) => {
socket.destroy();
reject(error);
});
socket.on('timeout', (error) => {
socket.destroy();
reject(error || 'socket TIMEOUT');
});
function milliseconds() {
const hrend = process.hrtime(hrstart);
const ms = hrend[0] * 1e3 + hrend[1] / 1e6;
return Math.floor(ms);
}
});
}
async checkTcp(address: string, port: number) {
try {
const ms = await this.checkTcpProcess(address, port);
this.message(`Node ${address}:${port} is good: ${ms} ms.`);
return true;
} catch (e) {
this.message(`Node ${address}:${port} failed: ${e.toString()}`);
return false;
}
}
async checkIcmp(address: string) {
try {
await ping(address, this.config.timeout);
this.message(`Node ${address}:ICMP is good.`);
return true;
} catch (e) {
this.message(`Node ${address}:ICMP failed: ${e.toString()}`);
return false;
}
}
constructor(config: Config) {
this.config = config;
this.client = new Aliyun(config.aliyun);
this.cdnRecordsRegex = config.cdnRecords.map(m => new RegExp(m.match));
this.id = ++Checker.order;
this.checkMethods = new Map();
this.checkMethods.set('http', (protocol: string, address: string, port: number) => {
return this.checkHttpOrHttps(protocol, address, port);
});
this.checkMethods.set('https', (protocol: string, address: string, port: number) => {
return this.checkHttpOrHttps(protocol, address, port);
});
this.checkMethods.set('tcp', (protocol: string, address: string, port: number) => {
return this.checkTcp(address, port);
});
this.checkMethods.set('icmp', (protocol: string, address: string, port: number) => {
return this.checkIcmp(address);
});
}
private message(msg: string) {
console.log(`${this.id} => ${msg}`);
......@@ -108,33 +198,23 @@ class Checker {
for (let record of ret.DomainRecords.Record.filter(m => {
return m.RR && m.Type === "CNAME" && _.any(this.cdnRecordsRegex, r => !!m.RR.match(r))
})) {
const port = _.find(this.config.cdnRecords, r => record.RR.match(r.match)).port;
const matchCDNRecord = _.find(this.config.cdnRecords, r => record.RR.match(r.match));
const { port, protocol } = matchCDNRecord;
const isCDN = this.isCDNRecord(record);
const recordInfo: DomainRecordInfo = { record, port, isCDN, good: false };
const recordInfo: DomainRecordInfo = { record, protocol, port, isCDN, good: false };
this.message(`Found record ${this.getRecordPattern(recordInfo)}`);
res.push(recordInfo);
}
}
return res;
}
async checkNode(address: string, port: number): Promise<boolean> {
async checkNode(protocol: string, address: string, port: number): Promise<boolean> {
let currentTestDomain: string;
const checkMethodFunction = this.checkMethods.get(protocol);
assert(checkMethodFunction, `Check method ${protocol} not supported.`);
for (let i = 1; i <= this.config.retryCount; ++i) {
try {
for (let testDomain of this.config.testDomains) {
currentTestDomain = testDomain;
await axios.get(`https://${address}:${port}`, {
headers: {
Host: testDomain
},
timeout: this.config.timeout,
validateStatus: status => status < 500
});
}
this.message(`Node ${address}:${port} is good.`);
if (await checkMethodFunction(protocol, address, port)) {
return true;
} catch (e) {
this.message(`Node ${address}:${port} Failed in checking ${currentTestDomain} ${i}: ${e.toString()}`);
}
}
this.message(`Node ${address}:${port} is bad.`);
......@@ -143,7 +223,7 @@ class Checker {
async checkRecord(recordInfo: DomainRecordInfo) {
const record = recordInfo.record;
this.message(`Checking record ${this.getRecordPattern(recordInfo)} with old status of ${record.Status}.`)
const good = await this.checkNode(record.Value, recordInfo.port);
const good = await this.checkNode(recordInfo.protocol, record.Value, recordInfo.port);
await this.handleRecordResult(recordInfo, good);
}
isRecordGood(recordInfo: DomainRecordInfo): boolean {
......@@ -198,7 +278,10 @@ async function run() {
async function main() {
config = YAML.parse(await fs.promises.readFile("./config.yaml", "utf8"));
//await run();
(new CronJob(config.cronString, run, null, true, "Asia/Shanghai", null, true)).start();
while (true) {
await run();
Q.delay(config.interval);
}
}
main();
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