zan-proxy
Version:
391 lines (360 loc) • 10.9 kB
text/typescript
import { promisify } from 'es6-promisify';
import EventEmitter from 'events';
import fs from 'fs';
import jsonfile from 'jsonfile';
import { cloneDeep, forEach, lowerCase } from 'lodash';
import fetch from 'node-fetch';
import path from 'path';
import { Service } from 'typedi';
import uuid from 'uuid/v4';
import { AppInfoService } from './appInfo';
const jsonfileWriteFile = promisify(jsonfile.writeFile);
const fsUnlink = promisify(fs.unlink);
export const ErrNameExists = new Error('name exists');
export interface Rule {
name: string;
key: string;
method: string;
match: string;
checked: boolean;
actionList: RuleAction[];
}
export interface RuleAction {
type: string;
data: RuleActionData;
}
export interface RuleActionData {
target: string;
dataId: string;
headerKey: string;
headerValue: string;
}
export interface RuleFile {
meta: RuleFileMeta;
checked: boolean;
name: string;
description: string;
content: Rule[];
disableSync?: boolean;
}
export interface RuleFileMeta {
remote: boolean;
url?: string;
ETag?: string;
remoteETag?: string;
}
export class RuleService extends EventEmitter {
public rules: object;
private usingRuleCache: object;
private ruleSaveDir: string;
constructor(appInfoService: AppInfoService) {
super();
// userId - > (filename -> rule)
this.rules = {};
// 缓存数据: 正在使用的规则 userId -> inUsingRuleList
this.usingRuleCache = {};
const proxyDataDir = appInfoService.getProxyDataDir();
this.ruleSaveDir = path.join(proxyDataDir, 'rule');
const contentMap = fs
.readdirSync(this.ruleSaveDir)
.filter(name => name.endsWith('.json'))
.reduce((prev, curr) => {
try {
prev[curr] = jsonfile.readFileSync(path.join(this.ruleSaveDir, curr));
} catch (e) {
// ignore
}
return prev;
}, {});
forEach(contentMap, (content, fileName) => {
const ruleName = content.name;
const userId = fileName.substr(
0,
this._getUserIdLength(fileName, ruleName),
);
this.rules[userId] = this.rules[userId] || {};
this.rules[userId][ruleName] = content;
});
}
// 创建规则文件
public async createRuleFile(userId, name, description) {
if (this.rules[userId] && this.rules[userId][name]) {
return ErrNameExists;
}
const ruleFile = {
checked: false,
content: [],
description,
meta: {
ETag: '',
remote: false,
remoteETag: '',
url: '',
},
name,
};
this.rules[userId] = this.rules[userId] || {};
this.rules[userId][name] = ruleFile;
// 写文件
const filePath = this._getRuleFilePath(userId, name);
await jsonfileWriteFile(filePath, ruleFile, { encoding: 'utf-8' });
// 发送消息通知
this.emit('data-change', userId, this.getRuleFileList(userId));
return true;
}
// 返回用户的规则文件列表
public getRuleFileList(userId) {
const ruleMap = (this.rules[userId] = this.rules[userId] || {});
const rulesLocal: any[] = [];
const rulesRemote: any[] = [];
forEach(ruleMap, content => {
if (content.meta&&content.meta.remote) {
rulesRemote.push({
checked: content.checked,
description: content.description,
disableSync: content.disableSync,
meta: content.meta,
name: content.name,
});
} else {
rulesLocal.push({
checked: content.checked,
description: content.description,
meta: content.meta,
name: content.name,
});
}
});
return rulesLocal.concat(rulesRemote);
}
// 删除规则文件
public async deleteRuleFile(userId, name) {
const rule = this.rules[userId][name];
delete this.rules[userId][name];
const ruleFilePath = this._getRuleFilePath(userId, name);
await fsUnlink(ruleFilePath);
// 发送消息通知
this.emit('data-change', userId, this.getRuleFileList(userId));
if (rule.checked) {
// 清空缓存
delete this.usingRuleCache[userId];
}
}
// 设置规则文件的使用状态
public async setRuleFileCheckStatus(userId, name, checked) {
this.rules[userId][name].checked = checked;
const ruleFilePath = this._getRuleFilePath(userId, name);
await jsonfileWriteFile(ruleFilePath, this.rules[userId][name], {
encoding: 'utf-8',
});
// 发送消息通知
this.emit('data-change', userId, this.getRuleFileList(userId));
delete this.usingRuleCache[userId];
}
// 设置规则文件的禁用同步状态
public async setRuleFileDisableSync(userId, name, disable) {
this.rules[userId][name].disableSync = disable;
const ruleFilePath = this._getRuleFilePath(userId, name);
await jsonfileWriteFile(ruleFilePath, this.rules[userId][name], {
encoding: 'utf-8',
});
// 发送消息通知
this.emit('data-change', userId, this.getRuleFileList(userId));
delete this.usingRuleCache[userId];
}
// 获取规则文件的内容
public getRuleFile(userId, name) {
return this.rules[userId][name];
}
// 保存规则文件(可能是远程、或者本地)
public async saveRuleFile(userId, ruleFile: RuleFile) {
const userRuleMap = this.rules[userId] || {};
const originRuleFile = userRuleMap[ruleFile.name];
if (originRuleFile) {
ruleFile.checked = originRuleFile.checked;
ruleFile.meta = originRuleFile.meta;
}
userRuleMap[ruleFile.name] = ruleFile;
this.rules[userId] = userRuleMap;
// 写文件
const filePath = this._getRuleFilePath(userId, ruleFile.name);
await jsonfileWriteFile(filePath, userRuleMap[ruleFile.name], {
encoding: 'utf-8',
});
// 清空缓存
delete this.usingRuleCache[userId];
this.emit('data-change', userId, this.getRuleFileList(userId));
}
// 修改规则文件名称
public async updateFileInfo(
userId,
originName: string,
{
name,
description,
}: {
name: string;
description: string;
},
) {
const userRuleMap = this.rules[userId] || {};
if (userRuleMap[name] && name !== originName) {
throw ErrNameExists;
}
const ruleFile = userRuleMap[originName];
// 删除旧的rule
delete this.rules[userId][originName];
const ruleFilePath = this._getRuleFilePath(userId, originName);
await fsUnlink(ruleFilePath);
// 修改rule名称
ruleFile.name = name;
ruleFile.description = description;
await this.saveRuleFile(userId, ruleFile);
}
/**
* 根据请求,获取处理请求的规则
* @param method
* @param urlObj
*/
public getProcessRule(userId, method, urlObj): Rule | null {
let candidateRule = null;
const inusingRules = this._getInuseRules(userId);
for (const rule of inusingRules) {
// 捕获规则
if (
this._isUrlMatch(urlObj.href, rule.match) &&
this._isMethodMatch(method, rule.method)
) {
candidateRule = rule;
break;
}
}
return candidateRule;
}
public async importRemoteRuleFile(userId, url): Promise<RuleFile> {
const ruleFile = await this.fetchRemoteRuleFile (url);
ruleFile.content.forEach(rule => {
if (rule.action && !rule.actionList) {
rule.actionList = [rule.action];
}
});
await this.saveRuleFile(userId, ruleFile);
return ruleFile;
}
public async fetchRemoteRuleFile(url): Promise<any> {
const response = await fetch(url);
const responseData = await response.json();
const ETag = response.headers.etag || '';
const content = responseData.content.map(remoteRule => {
if (remoteRule.action && !remoteRule.actionList) {
remoteRule.actionList = [remoteRule.action];
}
const actionList = remoteRule.actionList.map(remoteAction => {
const actionData: RuleActionData = {
dataId: '',
headerKey: remoteAction.data.headerKey || '',
headerValue: remoteAction.data.headerValue || '',
target: remoteAction.data.target || '',
};
const action: RuleAction = {
data: actionData,
type: remoteAction.type,
};
return action;
});
const rule: Rule = {
actionList,
checked: remoteRule.checked,
key: remoteRule.key || uuid(),
match: remoteRule.match,
method: remoteRule.method,
name: remoteRule.name,
};
return rule;
});
const ruleFile = {
checked: false,
content,
description: responseData.description,
meta: {
ETag,
remote: true,
remoteETag: ETag,
url,
},
name: responseData.name,
};
return ruleFile;
}
public async copyRuleFile(userId, name) {
const ruleFile = this.getRuleFile(userId, name);
if (!ruleFile) {
return;
}
const copied = cloneDeep(ruleFile);
copied.checked = false;
copied.name = `${copied.name}-复制`;
copied.meta = Object.assign({}, copied.meta, {
isCopy: true,
remote: false,
});
await this.saveRuleFile(userId, copied);
return copied;
}
private _getInuseRules(userId) {
if (this.usingRuleCache[userId]) {
return this.usingRuleCache[userId];
}
const ruleMap = this.rules[userId] || {};
// 计算使用中的规则
const rulesLocal: any[] = [];
const rulesRemote: any[] = [];
forEach(ruleMap, (file, filename) => {
if (!file.checked) {
return;
}
forEach(file.content, rule => {
if (!rule.checked) {
return;
}
const copy = cloneDeep(rule);
copy.ruleFileName = filename;
if (file.meta&&file.meta.remote) {
rulesRemote.push(copy);
} else {
rulesLocal.push(copy);
}
});
});
const merged = rulesLocal.concat(rulesRemote);
this.usingRuleCache[userId] = merged;
return merged;
}
private _getRuleFilePath(userId, ruleName) {
const fileName = `${userId}_${ruleName}.json`;
const filePath = path.join(this.ruleSaveDir, fileName);
return filePath;
}
private _getUserIdLength(ruleFileName, ruleName) {
return ruleFileName.length - ruleName.length - 6;
}
// 请求的方法是否匹配规则
private _isMethodMatch(reqMethod, ruleMethod) {
const loweredReqMethod = lowerCase(reqMethod);
const loweredRuleMethod = lowerCase(ruleMethod);
return (
loweredReqMethod === loweredRuleMethod ||
!ruleMethod ||
loweredReqMethod === 'option'
);
}
// 请求的url是否匹配规则
private _isUrlMatch(reqUrl, ruleMatchStr) {
return (
ruleMatchStr &&
(reqUrl.indexOf(ruleMatchStr) >= 0 ||
new RegExp(ruleMatchStr).test(reqUrl))
);
}
}