verdaccio
Version:
A lightweight private npm proxy registry
496 lines (494 loc) • 17.7 kB
JavaScript
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: "Module" }
});
const require_runtime = require("../_virtual/_rolldown/runtime.js");
const require_lib_constants = require("./constants.js");
const require_lib_logger_index = require("./logger/index.js");
const require_lib_utils = require("./utils.js");
let lodash = require("lodash");
lodash = require_runtime.__toESM(lodash);
let _verdaccio_utils = require("@verdaccio/utils");
let debug = require("debug");
debug = require_runtime.__toESM(debug);
let stream = require("stream");
stream = require_runtime.__toESM(stream);
let _verdaccio_streams = require("@verdaccio/streams");
let _cypress_request = require("@cypress/request");
_cypress_request = require_runtime.__toESM(_cypress_request);
let JSONStream = require("JSONStream");
JSONStream = require_runtime.__toESM(JSONStream);
let zlib = require("zlib");
zlib = require_runtime.__toESM(zlib);
let _verdaccio_core = require("@verdaccio/core");
//#region src/lib/up-storage.ts
var debug$1 = (0, debug.default)("verdaccio:proxy");
var encode = function(thing) {
return encodeURIComponent(thing).replace(/^%40/, "@");
};
var contentTypeAccept = `${_verdaccio_core.HEADERS.JSON};`;
/**
* Just a helper (`config[key] || default` doesn't work because of zeroes)
*/
var setConfig = (config, key, def) => {
return lodash.default.isNil(config[key]) === false ? config[key] : def;
};
/**
* Implements Storage interface
* (same for storage.js, local-storage.js, up-storage.js)
*/
var ProxyStorage = class {
config;
failed_requests;
userAgent;
ca;
logger;
server_id;
url;
maxage;
timeout;
max_fails;
fail_timeout;
agent_options;
upname;
proxy;
last_request_time;
strict_ssl;
/**
* Constructor
* @param {*} config
* @param {*} mainConfig
*/
constructor(config, mainConfig) {
this.config = config;
this.failed_requests = 0;
this.userAgent = mainConfig.user_agent ?? "hidden";
this.ca = config.ca;
this.logger = require_lib_logger_index.logger;
this.server_id = mainConfig.server_id;
try {
this.url = new URL(this.config.url);
} catch (err) {
const redactedUrl = String(this.config.url).replace(/\/\/[^/@]+@/, "//***@");
throw new Error(`invalid uplink url: ${redactedUrl}`, { cause: err });
}
this._setupProxy(this.url.hostname, config, mainConfig, this.url.protocol === "https:");
this.config.url = this.config.url.replace(/\/$/, "");
if (this.config.timeout && Number(this.config.timeout) >= 1e3) this.logger.warn([
"Too big timeout value: " + this.config.timeout,
"We changed time format to nginx-like one",
"(see http://nginx.org/en/docs/syntax.html)",
"so please update your config accordingly"
].join("\n"));
this.maxage = require_lib_utils.parseInterval(setConfig(this.config, "maxage", "2m"));
this.timeout = require_lib_utils.parseInterval(setConfig(this.config, "timeout", "30s"));
this.max_fails = Number(setConfig(this.config, "max_fails", 2));
this.fail_timeout = require_lib_utils.parseInterval(setConfig(this.config, "fail_timeout", "5m"));
this.strict_ssl = Boolean(setConfig(this.config, "strict_ssl", true));
this.agent_options = setConfig(this.config, "agent_options", {
keepAlive: true,
maxSockets: 40,
maxFreeSockets: 10
});
}
/**
* Fetch an asset.
* @param {*} options
* @param {*} cb
* @return {Request}
*/
request(options, cb) {
let json;
if (this._statusCheck() === false) {
const streamRead = new stream.default.Readable();
process.nextTick(function() {
if (cb) cb(require_lib_utils.ErrorCode.getInternalError(_verdaccio_core.API_ERROR.UPLINK_OFFLINE));
streamRead.emit("error", require_lib_utils.ErrorCode.getInternalError(_verdaccio_core.API_ERROR.UPLINK_OFFLINE));
});
streamRead._read = function() {};
streamRead.on("error", function() {});
return streamRead;
}
const self = this;
const headers = this._setHeaders(options);
this._addProxyHeaders(options.req, headers);
this._overrideWithUpLinkConfigHeaders(headers);
const method = options.method || "GET";
const uri = options.uri_full || this.config.url + options.uri;
self.logger.info({
method,
uri
}, "making request: '@{method} @{uri}'");
if (require_lib_utils.isObject(options.json)) {
json = JSON.stringify(options.json);
headers["Content-Type"] = headers["Content-Type"] || _verdaccio_core.HEADERS.JSON;
}
const requestCallback = cb ? function(err, res, body) {
let error;
const responseLength = err ? 0 : body.length;
processBody();
logActivity();
cb(err, res, body);
/**
* Perform a decode.
*/
function processBody() {
if (err) {
error = err.message;
return;
}
if (options.json && res.statusCode < 300) try {
body = JSON.parse(body.toString(_verdaccio_core.CHARACTER_ENCODING.UTF8));
} catch (_err) {
body = {};
err = _err;
error = err.message;
}
if (!err && require_lib_utils.isObject(body)) {
if (lodash.default.isString(body.error)) error = body.error;
}
}
/**
* Perform a log.
*/
function logActivity() {
let message = "@{!status}, req: '@{request.method} @{request.url}'";
message += error ? ", error: @{!error}" : ", bytes: @{bytes.in}/@{bytes.out}";
self.logger.http({
err: err || void 0,
request: {
method,
url: uri
},
status: res != null ? res.statusCode : "ERR",
error,
bytes: {
in: json ? json.length : 0,
out: responseLength || 0
}
}, message);
}
} : void 0;
let requestOptions = {
url: uri,
method,
headers,
body: json,
proxy: this.proxy,
encoding: null,
gzip: true,
timeout: this.timeout,
strictSSL: this.strict_ssl,
agentOptions: this.agent_options
};
if (this.ca) requestOptions = Object.assign({}, requestOptions, { ca: this.ca });
const req = (0, _cypress_request.default)(requestOptions, requestCallback);
let statusCalled = false;
req.on("response", function(res) {
if (!req._verdaccio_aborted && !statusCalled) {
statusCalled = true;
self._statusCheck(true);
}
if (lodash.default.isNil(requestCallback) === false) (function do_log() {
self.logger.http({
request: {
method,
url: uri
},
status: lodash.default.isNull(res) === false ? res.statusCode : "ERR"
}, "@{!status}, req: '@{request.method} @{request.url}' (streaming)");
})();
});
req.on("error", function(err) {
debug$1("_verdaccio_aborted: %o", err);
if (!req._verdaccio_aborted && !statusCalled) {
statusCalled = true;
self._statusCheck(false);
}
});
return req;
}
/**
* Set default headers.
* @param {Object} options
* @return {Object}
* @private
*/
_setHeaders(options) {
const headers = options.headers || {};
const accept = _verdaccio_core.HEADERS.ACCEPT;
const acceptEncoding = _verdaccio_core.HEADERS.ACCEPT_ENCODING;
const userAgent = _verdaccio_core.HEADERS.USER_AGENT;
headers[accept] = headers[accept] || contentTypeAccept;
headers[acceptEncoding] = headers[acceptEncoding] || "gzip";
headers[userAgent] = this.userAgent ? `npm (${this.userAgent})` : options.req?.get("user-agent");
return this._setAuth(headers);
}
/**
* Validate configuration auth and assign Header authorization
* @param {Object} headers
* @return {Object}
* @private
*/
_setAuth(headers) {
const { auth } = this.config;
if (lodash.default.isNil(auth) || headers[_verdaccio_core.HEADERS.AUTHORIZATION]) return headers;
if (lodash.default.isObject(auth) === false && lodash.default.isObject(auth.token) === false) this._throwErrorAuth("Auth invalid");
let token;
const tokenConf = auth;
if (lodash.default.isNil(tokenConf.token) === false && lodash.default.isString(tokenConf.token)) token = tokenConf.token;
else if (lodash.default.isNil(tokenConf.token_env) === false) if (lodash.default.isString(tokenConf.token_env)) token = process.env[tokenConf.token_env];
else if (lodash.default.isBoolean(tokenConf.token_env) && tokenConf.token_env) token = process.env.NPM_TOKEN;
else {
this.logger.error(require_lib_constants.ERROR_CODE.token_required);
this._throwErrorAuth(require_lib_constants.ERROR_CODE.token_required);
}
else token = process.env.NPM_TOKEN;
if (lodash.default.isNil(token)) this._throwErrorAuth(require_lib_constants.ERROR_CODE.token_required);
const type = tokenConf.type || _verdaccio_core.TOKEN_BASIC;
this._setHeaderAuthorization(headers, type, token);
return headers;
}
/**
* @param {string} message
* @throws {Error}
* @private
*/
_throwErrorAuth(message) {
this.logger.error(message);
throw new Error(message);
}
/**
* Assign Header authorization with type authentication
* @param {Object} headers
* @param {string} type
* @param {string} token
* @private
*/
_setHeaderAuthorization(headers, type, token) {
const _type = type.toLowerCase();
if ([_verdaccio_core.TOKEN_BEARER.toLowerCase(), _verdaccio_core.TOKEN_BASIC.toLowerCase()].includes(_type) === false) this._throwErrorAuth(`Auth type '${_type}' not allowed`);
type = lodash.default.upperFirst(type);
headers[_verdaccio_core.HEADERS.AUTHORIZATION] = (0, _verdaccio_utils.buildToken)(type, token);
}
/**
* It will add or override specified headers from config file.
*
* Eg:
*
* uplinks:
npmjs:
url: https://registry.npmjs.org/
headers:
Accept: "application/vnd.npm.install-v2+json; q=1.0"
verdaccio-staging:
url: https://mycompany.com/npm
headers:
Accept: "application/json"
authorization: "Basic YourBase64EncodedCredentials=="
* @param {Object} headers
* @private
*/
_overrideWithUpLinkConfigHeaders(headers) {
if (!this.config.headers) return headers;
for (const key in this.config.headers) headers[key] = this.config.headers[key];
}
/**
* Determine whether can fetch from the provided URL
* @param {*} url
* @return {Boolean}
*/
isUplinkValid(url) {
const urlParsed = URL.parse(url);
if (urlParsed === null) return false;
const isHTTPS = (urlDomainParsed) => urlDomainParsed.protocol === "https:" && (urlDomainParsed.port === "" || urlDomainParsed.port === "443");
const getHost = (urlDomainParsed) => isHTTPS(urlDomainParsed) ? urlDomainParsed.hostname : urlDomainParsed.host;
const getPath = (urlDomainParsed) => urlDomainParsed.pathname + urlDomainParsed.search;
const isMatchProtocol = urlParsed.protocol === this.url.protocol;
const isMatchHost = getHost(urlParsed) === getHost(this.url);
const isMatchPath = getPath(urlParsed).indexOf(getPath(this.url)) === 0;
return isMatchProtocol && isMatchHost && isMatchPath;
}
/**
* Get a remote package metadata
* @param {*} name package name
* @param {*} options request options, eg: eTag.
* @param {*} callback
*/
getRemoteMetadata(name, options, callback) {
const headers = {};
if (lodash.default.isNil(options.etag) === false) {
headers["If-None-Match"] = options.etag;
headers[_verdaccio_core.HEADERS.ACCEPT] = contentTypeAccept;
}
this.request({
uri: `/${encode(name)}`,
json: true,
headers,
req: options.req
}, (err, res, body) => {
if (err) return callback(err);
if (res.statusCode === _verdaccio_core.HTTP_STATUS.NOT_FOUND) return callback(require_lib_utils.ErrorCode.getNotFound(_verdaccio_core.API_ERROR.NOT_PACKAGE_UPLINK));
if (!(res.statusCode >= _verdaccio_core.HTTP_STATUS.OK && res.statusCode < _verdaccio_core.HTTP_STATUS.MULTIPLE_CHOICES)) {
const error = require_lib_utils.ErrorCode.getInternalError(`${_verdaccio_core.API_ERROR.BAD_STATUS_CODE}: ${res.statusCode}`);
error.remoteStatus = res.statusCode;
return callback(error);
}
callback(null, body, res.headers.etag);
});
}
/**
* Fetch a tarball from the uplink.
* @param {String} url
* @return {Stream}
*/
fetchTarball(url) {
const stream$1 = new _verdaccio_streams.ReadTarball({});
let current_length = 0;
let expected_length;
stream$1.abort = () => {};
const readStream = this.request({
uri_full: url,
encoding: null,
headers: { Accept: contentTypeAccept }
});
readStream.on("response", function(res) {
if (res.statusCode === _verdaccio_core.HTTP_STATUS.NOT_FOUND) return stream$1.emit("error", require_lib_utils.ErrorCode.getNotFound(_verdaccio_core.API_ERROR.NOT_FILE_UPLINK));
if (res.statusCode === _verdaccio_core.HTTP_STATUS.FORBIDDEN) {
const chunks = [];
readStream.on("data", (chunk) => chunks.push(chunk));
readStream.once("end", () => {
let body;
const raw = Buffer.concat(chunks);
if (raw.length > 0) try {
body = JSON.parse(raw.toString("utf8"));
} catch {}
const message = body?.detail || body?.title || body?.message || "tarball not available: forbidden by uplink";
stream$1.emit("error", require_lib_utils.ErrorCode.getForbidden(message));
});
return;
}
if (!(res.statusCode >= _verdaccio_core.HTTP_STATUS.OK && res.statusCode < _verdaccio_core.HTTP_STATUS.MULTIPLE_CHOICES)) return stream$1.emit("error", require_lib_utils.ErrorCode.getInternalError(`bad uplink status code: ${res.statusCode}`));
if (res.headers[_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH]) {
expected_length = res.headers[_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH];
stream$1.emit(_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH, res.headers[_verdaccio_core.HEADER_TYPE.CONTENT_LENGTH]);
}
readStream.pipe(stream$1);
});
readStream.on("error", function(err) {
stream$1.emit("error", err);
});
readStream.on("data", function(data) {
current_length += data.length;
});
readStream.on("end", function(data) {
if (data) current_length += data.length;
if (expected_length && current_length != expected_length) stream$1.emit("error", require_lib_utils.ErrorCode.getInternalError(_verdaccio_core.API_ERROR.CONTENT_MISMATCH));
});
return stream$1;
}
/**
* Perform a stream search.
* @param {*} options request options
* @return {Stream}
*/
search(options) {
const transformStream = new stream.default.PassThrough({ objectMode: true });
const requestStream = this.request({
uri: options.url || options.req.url,
req: options.req,
headers: { referer: options.req.get("referer") }
});
const parsePackage = (pkg) => {
if (require_lib_utils.isObjectOrArray(pkg)) transformStream.emit("data", pkg);
};
requestStream.on("response", (res) => {
if (!String(res.statusCode).match(/^2\d\d$/)) return transformStream.emit("error", require_lib_utils.ErrorCode.getInternalError(`bad status code ${res.statusCode} from uplink`));
let jsonStream;
if (res.headers[_verdaccio_core.HEADER_TYPE.CONTENT_ENCODING] === _verdaccio_core.HEADERS.GZIP) jsonStream = res.pipe(zlib.default.createUnzip());
else jsonStream = res;
jsonStream.pipe(JSONStream.default.parse("*")).on("data", parsePackage);
jsonStream.on("end", () => {
transformStream.emit("end");
});
});
requestStream.on("error", (err) => {
transformStream.emit("error", err);
});
transformStream.abort = () => {
requestStream.abort();
transformStream.emit("end");
};
return transformStream;
}
/**
* Add proxy headers.
* FIXME: object mutations, it should return an new object
* @param {*} req the http request
* @param {*} headers the request headers
*/
_addProxyHeaders(req, headers) {
if (req) {
if (!this.proxy) headers["x-forwarded-for"] = (req.get("x-forwarded-for") ? req.get("x-forwarded-for") + ", " : "") + req.connection.remoteAddress;
}
headers["via"] = req && req.get("via") ? req.get("via") + ", " : "";
headers["via"] += "1.1 " + this.server_id + " (Verdaccio)";
}
/**
* Check whether the remote host is available.
* @param {*} alive
* @return {Boolean}
*/
_statusCheck(alive) {
if (arguments.length === 0) return this._ifRequestFailure() === false;
if (alive) {
if (this.failed_requests >= this.max_fails) this.logger.warn({ host: this.url.host }, "host @{host} is back online");
this.failed_requests = 0;
} else {
this.failed_requests++;
if (this.failed_requests === this.max_fails) this.logger.warn({ host: this.url.host }, "host @{host} is now offline");
}
this.last_request_time = Date.now();
}
/**
* If the request failure.
* @return {boolean}
* @private
*/
_ifRequestFailure() {
return this.failed_requests >= this.max_fails && Math.abs(Date.now() - this.last_request_time) < this.fail_timeout;
}
/**
* Set up a proxy.
* @param {*} hostname
* @param {*} config
* @param {*} mainconfig
* @param {*} isHTTPS
*/
_setupProxy(hostname, config, mainconfig, isHTTPS) {
let noProxyList;
const proxy_key = isHTTPS ? "https_proxy" : "http_proxy";
if (proxy_key in config) this.proxy = config[proxy_key];
else if (proxy_key in mainconfig) this.proxy = mainconfig[proxy_key];
if ("no_proxy" in config) noProxyList = config.no_proxy;
else if ("no_proxy" in mainconfig) noProxyList = mainconfig.no_proxy;
if (hostname[0] !== ".") hostname = "." + hostname;
if (lodash.default.isString(noProxyList) && noProxyList.length) noProxyList = noProxyList.split(",");
if (lodash.default.isArray(noProxyList)) for (let i = 0; i < noProxyList.length; i++) {
let noProxyItem = noProxyList[i];
if (noProxyItem[0] !== ".") noProxyItem = "." + noProxyItem;
if (hostname.lastIndexOf(noProxyItem) === hostname.length - noProxyItem.length) {
if (this.proxy) {
debug$1("not using proxy for %o, excluded by %o rule", this.url.href, noProxyItem);
this.proxy = false;
}
break;
}
}
if (lodash.default.isString(this.proxy) === false) delete this.proxy;
else debug$1("using proxy %o for %o", this.url.href, this.proxy);
}
};
//#endregion
exports.default = ProxyStorage;
//# sourceMappingURL=up-storage.js.map