From 5bb81ebf9129488bf1d34a3be640a052b80893df Mon Sep 17 00:00:00 2001 From: Marc Bernard <59966492+mbtools@users.noreply.github.com> Date: Tue, 1 Oct 2024 02:31:42 -0400 Subject: [PATCH] fix(middleware): encoding of scoped package name (#4874) * fix(middleware): encoding of scope package name * Change order * Test description * debug * Add to tests --- .changeset/nine-countries-remember.md | 5 ++ .../middleware/src/middlewares/encode-pkg.ts | 11 +++ packages/middleware/test/encode.spec.ts | 82 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 .changeset/nine-countries-remember.md diff --git a/.changeset/nine-countries-remember.md b/.changeset/nine-countries-remember.md new file mode 100644 index 000000000..2acc290c9 --- /dev/null +++ b/.changeset/nine-countries-remember.md @@ -0,0 +1,5 @@ +--- +'@verdaccio/middleware': patch +--- + +fix(middleware): encoding of scope package name diff --git a/packages/middleware/src/middlewares/encode-pkg.ts b/packages/middleware/src/middlewares/encode-pkg.ts index 21c4c99b5..ba5ecac35 100644 --- a/packages/middleware/src/middlewares/encode-pkg.ts +++ b/packages/middleware/src/middlewares/encode-pkg.ts @@ -1,5 +1,9 @@ +import buildDebug from 'debug'; + import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types'; +const debug = buildDebug('verdaccio:middleware:encode'); + /** * Encode / in a scoped package name to be matched as a single parameter in routes * @param req @@ -11,9 +15,16 @@ export function encodeScopePackage( res: $ResponseExtend, next: $NextFunctionVer ): void { + const original = req.url; + // If the @ sign is encoded, we need to decode it first + // e.g.: /%40org/pkg/1.2.3 -> /@org/pkg/1.2.3 + if (req.url.indexOf('%40') !== -1) { + req.url = req.url.replace(/^\/%40/, '/@'); + } if (req.url.indexOf('@') !== -1) { // e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3 req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F'); } + debug('encodeScopePackage: %o -> %o', original, req.url); next(); } diff --git a/packages/middleware/test/encode.spec.ts b/packages/middleware/test/encode.spec.ts index a500a24c5..27e04d1c4 100644 --- a/packages/middleware/test/encode.spec.ts +++ b/packages/middleware/test/encode.spec.ts @@ -20,3 +20,85 @@ test('encode is json', async () => { expect(res.body).toEqual({ id: '@scope/foo' }); expect(res.status).toEqual(HTTP_STATUS.OK); }); + +test('packages with version/scope', async () => { + const app = getApp([]); + // @ts-ignore + app.use(encodeScopePackage); + // @ts-ignore + app.get('/:package/:version?', (req, res) => { + const { package: pkg, version } = req.params; + res.status(HTTP_STATUS.OK).json({ package: pkg, version }); + }); + + const res = await request(app).get('/foo'); + expect(res.body).toEqual({ package: 'foo' }); + expect(res.status).toEqual(HTTP_STATUS.OK); + + const res2 = await request(app).get('/foo/1.0.0'); + expect(res2.body).toEqual({ package: 'foo', version: '1.0.0' }); + expect(res2.status).toEqual(HTTP_STATUS.OK); + + const res3 = await request(app).get('/@scope/foo'); + expect(res3.body).toEqual({ package: '@scope/foo' }); + expect(res3.status).toEqual(HTTP_STATUS.OK); + + const res4 = await request(app).get('/@scope/foo/1.0.0'); + expect(res4.body).toEqual({ package: '@scope/foo', version: '1.0.0' }); + expect(res4.status).toEqual(HTTP_STATUS.OK); + + const res5 = await request(app).get('/@scope%2ffoo'); + expect(res5.body).toEqual({ package: '@scope/foo' }); + expect(res5.status).toEqual(HTTP_STATUS.OK); + + const res6 = await request(app).get('/@scope%2ffoo/1.0.0'); + expect(res6.body).toEqual({ package: '@scope/foo', version: '1.0.0' }); + expect(res6.status).toEqual(HTTP_STATUS.OK); + + const res7 = await request(app).get('/%40scope%2ffoo'); + expect(res7.body).toEqual({ package: '@scope/foo' }); + expect(res7.status).toEqual(HTTP_STATUS.OK); + + const res8 = await request(app).get('/%40scope%2ffoo/1.0.0'); + expect(res8.body).toEqual({ package: '@scope/foo', version: '1.0.0' }); + expect(res8.status).toEqual(HTTP_STATUS.OK); + + const res9 = await request(app).get('/%40scope/foo'); + expect(res9.body).toEqual({ package: '@scope/foo' }); + expect(res9.status).toEqual(HTTP_STATUS.OK); + + const res10 = await request(app).get('/%40scope/foo/1.0.0'); + expect(res10.body).toEqual({ package: '@scope/foo', version: '1.0.0' }); + expect(res10.status).toEqual(HTTP_STATUS.OK); +}); + +test('tarballs with and without scope', async () => { + const app = getApp([]); + // @ts-ignore + app.use(encodeScopePackage); + // @ts-ignore + app.get('/:package/-/:filename', (req, res) => { + const { package: pkg, filename } = req.params; + res.status(HTTP_STATUS.OK).json({ package: pkg, filename }); + }); + + const res = await request(app).get('/foo/-/foo-1.2.3.tgz'); + expect(res.body).toEqual({ package: 'foo', filename: 'foo-1.2.3.tgz' }); + expect(res.status).toEqual(HTTP_STATUS.OK); + + const res2 = await request(app).get('/@scope/foo/-/foo-1.2.3.tgz'); + expect(res2.body).toEqual({ package: '@scope/foo', filename: 'foo-1.2.3.tgz' }); + expect(res2.status).toEqual(HTTP_STATUS.OK); + + const res3 = await request(app).get('/@scope%2ffoo/-/foo-1.2.3.tgz'); + expect(res3.body).toEqual({ package: '@scope/foo', filename: 'foo-1.2.3.tgz' }); + expect(res3.status).toEqual(HTTP_STATUS.OK); + + const res4 = await request(app).get('/%40scope%2ffoo/-/foo-1.2.3.tgz'); + expect(res4.body).toEqual({ package: '@scope/foo', filename: 'foo-1.2.3.tgz' }); + expect(res4.status).toEqual(HTTP_STATUS.OK); + + const res5 = await request(app).get('/%40scope/foo/-/foo-1.2.3.tgz'); + expect(res5.body).toEqual({ package: '@scope/foo', filename: 'foo-1.2.3.tgz' }); + expect(res5.status).toEqual(HTTP_STATUS.OK); +});