Last active
December 11, 2017 05:09
-
-
Save leizongmin/fdfb6e07515f85ec3ac696664d86e5f2 to your computer and use it in GitHub Desktop.
私有NPM代理程序
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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