diff --git a/app/common/PackageUtil.ts b/app/common/PackageUtil.ts index c3e146e4..5e689dc5 100644 --- a/app/common/PackageUtil.ts +++ b/app/common/PackageUtil.ts @@ -3,6 +3,7 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import * as ssri from 'ssri'; import tar from 'tar'; +import { PackageJSONType } from '../repository/PackageRepository'; // /@cnpm%2ffoo // /@cnpm%2Ffoo @@ -102,3 +103,25 @@ export async function hasShrinkWrapInTgz(contentOrFile: Uint8Array | string): Pr throw Object.assign(new Error('[hasShrinkWrapInTgz] Fail to parse input file'), { cause: e }); } } + +export async function extractPackageJSON(tarballBytes: Buffer): Promise { + return new Promise((resolve, reject) => { + Readable.from(tarballBytes) + .pipe(tar.t({ + filter: name => name === 'package/package.json', + onentry: entry => { + let json = ''; + entry.on('data', data => { + json += data.toString(); + }); + entry.on('end', () => { + try { + resolve(JSON.parse(json)); + } catch (err) { + reject(new Error('Error parsing package.json')); + } + }); + }, + })); + }); +} diff --git a/app/port/config.ts b/app/port/config.ts index f5f7fa1c..b53f37c6 100644 --- a/app/port/config.ts +++ b/app/port/config.ts @@ -150,4 +150,9 @@ export type CnpmcoreConfig = { * in most cases, you should set to false to keep the same behavior as source registry. */ strictSyncSpecivicVersion: boolean, + + /** + * strictly enforces/validates manifest and tgz when publish, https://github.com/cnpm/cnpmcore/issues/542 + */ + strictlyValidateTarballPkg?: boolean, }; diff --git a/app/port/controller/package/SavePackageVersionController.ts b/app/port/controller/package/SavePackageVersionController.ts index 59e3dfa2..e1532e4b 100644 --- a/app/port/controller/package/SavePackageVersionController.ts +++ b/app/port/controller/package/SavePackageVersionController.ts @@ -1,4 +1,5 @@ import { PackageJson, Simplify } from 'type-fest'; +import { isEqual } from 'lodash'; import { UnprocessableEntityError, ForbiddenError, @@ -17,7 +18,7 @@ import * as ssri from 'ssri'; import validateNpmPackageName from 'validate-npm-package-name'; import { Static, Type } from '@sinclair/typebox'; import { AbstractController } from '../AbstractController'; -import { getScopeAndName, FULLNAME_REG_STRING } from '../../../common/PackageUtil'; +import { getScopeAndName, FULLNAME_REG_STRING, extractPackageJSON } from '../../../common/PackageUtil'; import { PackageManagerService } from '../../../core/service/PackageManagerService'; import { VersionRule, @@ -28,6 +29,8 @@ import { import { RegistryManagerService } from '../../../core/service/RegistryManagerService'; import { PackageJSONType } from '../../../repository/PackageRepository'; +const STRICT_CHECK_TARBALL_FIELDS: (keyof PackageJson)[] = [ 'name', 'version', 'scripts', 'dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies', 'license', 'licenses', 'bin' ]; + type PackageVersion = Simplify { + return !isEqual(tarballPkg[key], versionManifest[key]); + }); + if (diffKey) { + throw new UnprocessableEntityError(`${diffKey} mismatch between tarball and manifest`); + } + } + const [ scope, name ] = getScopeAndName(fullname); // make sure readme is string diff --git a/config/config.default.ts b/config/config.default.ts index 4be24121..0f6cfb78 100644 --- a/config/config.default.ts +++ b/config/config.default.ts @@ -54,6 +54,7 @@ export const cnpmcoreConfig: CnpmcoreConfig = { redirectNotFound: true, enableUnpkg: true, strictSyncSpecivicVersion: false, + strictlyValidateTarballPkg: false, }; export default (appInfo: EggAppConfig) => { diff --git a/test/port/controller/package/SavePackageVersionController.test.ts b/test/port/controller/package/SavePackageVersionController.test.ts index 2a794503..a60c5633 100644 --- a/test/port/controller/package/SavePackageVersionController.test.ts +++ b/test/port/controller/package/SavePackageVersionController.test.ts @@ -21,7 +21,7 @@ describe('test/port/controller/package/SavePackageVersionController.test.ts', () }); describe('[PUT /:fullname] save()', () => { - it('should set registry filed after publish', async () => { + it('should set registry field after publish', async () => { mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true); const { pkg, user } = await TestUtil.createPackage({ name: 'non_scope_pkg', version: '1.0.0' }); const pkg2 = await TestUtil.getFullPackage({ name: pkg.name, version: '2.0.0' }); @@ -86,6 +86,21 @@ describe('test/port/controller/package/SavePackageVersionController.test.ts', () assert(pkgEntity); assert.equal(pkgEntity.registryId, selfRegistry.registryId); }); + it('should verify tgz and manifest', async () => { + mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true); + const { pkg, user } = await TestUtil.createPackage({ name: 'non_scope_pkg', version: '1.0.0' }); + const pkg2 = await TestUtil.getFullPackage({ name: pkg.name, version: '2.0.0' }); + + mock(app.config.cnpmcore, 'strictlyValidateTarballPkg', true); + const res = await app.httpRequest() + .put(`/${pkg2.name}`) + .set('authorization', user.authorization) + .set('user-agent', user.ua) + .send(pkg2) + .expect(422); + + assert.equal(res.body.error, '[UNPROCESSABLE_ENTITY] name mismatch between tarball and manifest'); + }); it('should add new version success on scoped package', async () => { const name = '@cnpm/publish-package-test';