UNPKG

verdaccio

Version:

A lightweight private npm proxy registry

496 lines (494 loc) 17.7 kB
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