Skip to content

Instantly share code, notes, and snippets.

@leizongmin
Last active December 11, 2017 05:09
Show Gist options
  • Save leizongmin/fdfb6e07515f85ec3ac696664d86e5f2 to your computer and use it in GitHub Desktop.
Save leizongmin/fdfb6e07515f85ec3ac696664d86e5f2 to your computer and use it in GitHub Desktop.
私有NPM代理程序
const httpRequest = require("http").request;
const httpsRequest = require("https").request;
const parseUrl = require("url").parse;
const connect = require("connect");
const colors = require("colors");
const PORT = Number(process.env.PORT || 6666);
const REGISTRY_CDN =
process.env.REGISTRY_CDN || "https://registry.npm.taobao.org";
const REGISTRY_PRIVATE =
process.env.REGISTRY_PRIVATE || "https://sinopia.example.com";
const PRIVATE_AUTHTOKEN =
process.env.PRIVATE_AUTHTOKEN ||
"*********************";
const PACKAGE_PREFIX = process.env.PACKAGE_PREFIX || "@";
const app = connect();
app.use(function(req, res) {
// console.log(req.url.indexOf(PACKAGE_PREFIX) === 0, req.url, PACKAGE_PREFIX);
if (req.url.indexOf(PACKAGE_PREFIX) === 1) {
proxy(req, res, REGISTRY_PRIVATE);
} else {
proxy(req, res, REGISTRY_CDN);
}
});
console.log("CDN 地址:%s", REGISTRY_CDN);
console.log("私有地址:%s", REGISTRY_PRIVATE);
app.listen(PORT, () => console.log("监听端口:%s", PORT));
let counter = 0;
function proxy(req, res, target) {
const n = ++counter;
const t = Date.now();
const log = (...args) => outputLog(n, t, ...args);
log("%s %s 代理到:%s", req.method, req.url, target);
// console.log(req.headers);
const info = parseUrl(target);
info.port = Number(info.port || (isHttpsProtocol(info.protocol) ? 443 : 80));
const headers = { ...req.headers };
delete headers["connection"];
delete headers["host"];
if (target === REGISTRY_PRIVATE) {
// 私有 NPM 的 authtoken
headers["authorization"] = `Bearer ${PRIVATE_AUTHTOKEN}`;
}
const info2 = {
host: info.hostname,
port: info.port,
method: req.method,
path: req.url,
headers: headers
};
// 针对 CNPM 特殊情况
if (info2.path === "//binary-mirror-config/latest") {
info2.path = info2.path.slice(1);
}
// 针对 sinopia 特殊情况在 CNPM 上有 bug
let hook = false;
if (
target === REGISTRY_PRIVATE &&
req.method === "GET" &&
/\/[a-zA-Z0-9\-%@]/.test(req.url)
) {
delete info2.headers["accept-encoding"];
hook = (req, res, req2, res2) => {
log("改写 package 数据");
delete res2.headers["content-length"];
res.writeHead(res2.statusCode || 200, res2.headers);
const list = [];
res2.on("data", b => list.push(b));
res2.on("end", () => {
const buf = Buffer.concat(list);
let data;
try {
data = JSON.parse(buf.toString());
} catch (err) {
log(colors.red("解析 JSON 出错:%s"), err);
return res.end(buf);
}
for (const v in data.versions) {
const vf = data.versions[v];
if (vf && vf.dist && vf.dist.tarball) {
// 改写为 https
if (vf.dist.tarball.indexOf("http:") === 0) {
vf.dist.tarball = `https:${vf.dist.tarball.slice(5)}`;
}
// 更换域名为本地服务器
vf.dist.tarball = String(vf.dist.tarball).replace(
REGISTRY_PRIVATE,
`http://127.0.0.1:${PORT}`
);
}
}
res.end(JSON.stringify(data));
});
};
}
// log(info2);
const request = isHttpsProtocol(info.protocol) ? httpsRequest : httpRequest;
const req2 = request(info2, res2 => {
const status = res2.statusCode;
log(
"响应:%s",
status < 200 || status >= 400
? colors.yellow(status)
: colors.green(status)
);
res2.on("error", err => {
log("出错:%s", err);
res.end(String(err));
});
if (hook) {
return hook(req, res, req2, res2);
}
res.writeHead(res2.statusCode || 200, res2.headers);
res2.pipe(res);
});
req2.on("error", err => {
log(colors.red("出错:%s"), n, err);
});
req2.on("close", err => {
log("结束");
});
req.pipe(req2);
}
function isHttpsProtocol(protocol) {
protocol = protocol || "http:";
return protocol.toLowerCase() === "https:";
}
function outputLog() {
const args = Array.prototype.slice.call(arguments);
const n = args[0];
const t = args[1];
const s = args[2];
const a = args.slice(3);
const x = [
`%s %s ${s} %s`,
colors.gray(toISOString(new Date())),
colors.green(`[${n}]`),
...a,
colors.cyan(`+${Date.now() - t}ms`)
];
console.log.apply(console, x);
}
function toISOString(date) {
function pad(n) {
return n < 10 ? "0" + n : String(n);
}
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
"T" +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds()) +
"." +
(date.getMilliseconds() / 1000).toFixed(3).slice(2, 5) +
"+" +
pad(-date.getTimezoneOffset() / 60) +
":00"
);
}
// npm获取路径: https://sinopia.example.com/json5/-/json5-0.5.1.tgz
// cnpm获取路径:https://sinopia.example.com/@private%2fmodule/-/module-1.2.0.tgz
// cnpm --registry="http://127.0.0.1:6666/" i @gz/oss
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment