From 591ad20ee840b50aa9e0932587cb3f6d8d8a932e Mon Sep 17 00:00:00 2001 From: Juan Picado Date: Tue, 5 Dec 2023 21:26:59 +0100 Subject: [PATCH] fix: restore search all endpoint (#4233) --- src/api/endpoint/api/search.ts | 106 +++++++++++++++++++++++++++++++-- src/api/endpoint/index.ts | 2 +- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/api/endpoint/api/search.ts b/src/api/endpoint/api/search.ts index 7ca34ce11..1d6205668 100644 --- a/src/api/endpoint/api/search.ts +++ b/src/api/endpoint/api/search.ts @@ -1,11 +1,105 @@ -import { HTTP_STATUS } from '../../../lib/constants'; +import { API_ERROR, HEADERS } from '../../../lib/constants'; import { logger } from '../../../lib/logger'; -export default function (route): void { +export default function (route, auth, storage): void { // searching packages - route.get('/-/all(/since)?', function (_req, res) { - logger.warn('search endpoint has been removed, please use search v1'); - res.status(HTTP_STATUS.NOT_FOUND); - res.json({ error: 'not found, endpoint removed' }); + route.get('/-/all(/since)?', function (req, res, next) { + let received_end = false; + let response_finished = false; + let processing_pkgs = 0; + let firstPackage = true; + logger.warn('/-/all search endpoint is deprecated, might be removed in the next major release'); + res.status(200); + res.set(HEADERS.CONTENT_TYPE, HEADERS.JSON_CHARSET); + + /* + * Offical NPM registry (registry.npmjs.org) no longer return whole database, + * They only return packages matched with keyword in `referer: search pkg-name`, + * And NPM client will request server in every search. + * + * The magic number 99999 was sent by NPM registry. Modify it may caused strange + * behaviour in the future. + * + * BTW: NPM will not return result if user-agent does not contain string 'npm', + * See: method 'request' in up-storage.js + * + * If there is no cache in local, NPM will request /-/all, then get response with + * _updated: 99999, 'Date' in response header was Mon, 10 Oct 1983 00:12:48 GMT, + * this will make NPM always query from server + * + * Data structure also different, whel request /-/all, response is an object, but + * when request /-/all/since, response is an array + */ + const respShouldBeArray = req.path.endsWith('/since'); + if (!respShouldBeArray) { + res.set('Date', 'Mon, 10 Oct 1983 00:12:48 GMT'); + } + const check_finish = function (): void { + if (!received_end) { + return; + } + if (processing_pkgs) { + return; + } + if (response_finished) { + return; + } + response_finished = true; + if (respShouldBeArray) { + res.end(']\n'); + } else { + res.end('}\n'); + } + }; + + if (respShouldBeArray) { + res.write('['); + } else { + res.write('{"_updated":' + 99999); + } + + const stream = storage.search(req.query.startkey || 0, { req: req }); + + stream.on('data', function each(pkg) { + processing_pkgs++; + + auth.allow_access({ packageName: pkg.name }, req.remote_user, function (err, allowed) { + processing_pkgs--; + + if (err) { + if (err.status && String(err.status).match(/^4\d\d$/)) { + // auth plugin returns 4xx user error, + // that's equivalent of !allowed basically + allowed = false; + } else { + stream.abort(err); + } + } + + if (allowed) { + if (respShouldBeArray) { + res.write(`${firstPackage ? '' : ','}${JSON.stringify(pkg)}\n`); + if (firstPackage) { + firstPackage = false; + } + } else { + res.write(',\n' + JSON.stringify(pkg.name) + ':' + JSON.stringify(pkg)); + } + } + + check_finish(); + }); + }); + + stream.on('error', function (err) { + logger.error('search `/-/all endpoint has failed @{err}', err); + received_end = true; + check_finish(); + }); + + stream.on('end', function () { + received_end = true; + check_finish(); + }); }); } diff --git a/src/api/endpoint/index.ts b/src/api/endpoint/index.ts index dd8dd5e93..b30ceb1d7 100644 --- a/src/api/endpoint/index.ts +++ b/src/api/endpoint/index.ts @@ -49,7 +49,7 @@ export default function (config: Config, auth: Auth, storage: Storage) { app.use(encodeScopePackage); whoami(app); profile(app, auth, config); - search(app); + search(app, auth, storage); user(app, auth, config); distTags(app, auth, storage); publish(app, auth, storage, config);