Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add unpkg-white-list to detect sync unpkg files or not #686

Merged
merged 2 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';
import { readFile } from 'fs/promises';
import { Application } from 'egg';
import { ChangesStreamService } from './app/core/service/ChangesStreamService';

declare module 'egg' {
interface Application {
binaryHTML: string;
Expand Down
17 changes: 15 additions & 2 deletions app/core/event/SyncPackageVersionFile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Event, Inject } from '@eggjs/tegg';
import {
EggAppConfig,
EggAppConfig, EggLogger,
} from 'egg';
import { ForbiddenError } from 'egg-errors';
import { PACKAGE_VERSION_ADDED, PACKAGE_TAG_ADDED, PACKAGE_TAG_CHANGED } from './index';
import { getScopeAndName } from '../../common/PackageUtil';
import { PackageManagerService } from '../service/PackageManagerService';
Expand All @@ -11,6 +12,8 @@
@Inject()
protected readonly config: EggAppConfig;
@Inject()
protected readonly logger: EggLogger;
@Inject()
private readonly packageManagerService: PackageManagerService;
@Inject()
private readonly packageVersionFileService: PackageVersionFileService;
Expand All @@ -25,7 +28,17 @@
const { packageVersion } = await this.packageManagerService.showPackageVersionByVersionOrTag(
scope, name, version);
if (!packageVersion) return;
await this.packageVersionFileService.syncPackageVersionFiles(packageVersion);
try {
await this.packageVersionFileService.syncPackageVersionFiles(packageVersion);
} catch (err) {
if (err instanceof ForbiddenError) {
this.logger.info('[SyncPackageVersionFileEvent.syncPackageVersionFile] ignore sync files, cause: %s',
err.message,
);
return;
}
throw err;
}

Check warning on line 41 in app/core/event/SyncPackageVersionFile.ts

View check run for this annotation

Codecov / codecov/patch

app/core/event/SyncPackageVersionFile.ts#L34-L41

Added lines #L34 - L41 were not covered by tests
}

protected async syncPackageReadmeToLatestVersion(fullname: string) {
Expand Down
69 changes: 66 additions & 3 deletions app/core/service/PackageVersionFileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,34 @@ import {
SingletonProto,
Inject,
} from '@eggjs/tegg';
import { ConflictError, ForbiddenError } from 'egg-errors';
import semver from 'semver';
import { AbstractService } from '../../common/AbstractService';
import {
calculateIntegrity,
getFullname,
} from '../../common/PackageUtil';
import { createTempDir, mimeLookup } from '../../common/FileUtil';
import {
PackageRepository,
} from '../../repository/PackageRepository';
import { PackageVersionFileRepository } from '../../repository/PackageVersionFileRepository';
import { PackageVersionRepository } from '../../repository/PackageVersionRepository';
import { DistRepository } from '../../repository/DistRepository';
import { PackageVersionFile } from '../entity/PackageVersionFile';
import { PackageVersion } from '../entity/PackageVersion';
import { Package } from '../entity/Package';
import { PackageManagerService } from './PackageManagerService';
import { CacheAdapter } from '../../common/adapter/CacheAdapter';
import { ConflictError } from 'egg-errors';

const unpkgWhiteListUrl = 'https://github.com/cnpm/unpkg-white-list';

@SingletonProto({
accessLevel: AccessLevel.PUBLIC,
})
export class PackageVersionFileService extends AbstractService {
@Inject()
private readonly packageVersionRepository: PackageVersionRepository;
@Inject()
private readonly packageRepository: PackageRepository;
@Inject()
Expand All @@ -39,6 +46,12 @@ export class PackageVersionFileService extends AbstractService {
@Inject()
private readonly cacheAdapter: CacheAdapter;

#unpkgWhiteListCurrentVersion: string = '';
#unpkgWhiteListAllowPackages: Record<string, {
version: string;
}> = {};
#unpkgWhiteListAllowScopes: string[] = [];

async listPackageVersionFiles(pkgVersion: PackageVersion, directory: string) {
await this.#ensurePackageVersionFilesSync(pkgVersion);
return await this.packageVersionFileRepository.listPackageVersionFiles(pkgVersion.packageVersionId, directory);
Expand All @@ -54,16 +67,58 @@ export class PackageVersionFileService extends AbstractService {
async #ensurePackageVersionFilesSync(pkgVersion: PackageVersion) {
const hasFiles = await this.packageVersionFileRepository.hasPackageVersionFiles(pkgVersion.packageVersionId);
if (!hasFiles) {
const lockRes = await this.cacheAdapter.usingLock(`${pkgVersion.packageVersionId}:syncFiles`, 60, async () => {
const lockName = `${pkgVersion.packageVersionId}:syncFiles`;
const lockRes = await this.cacheAdapter.usingLock(lockName, 60, async () => {
await this.syncPackageVersionFiles(pkgVersion);
});
// lock fail
if (!lockRes) {
this.logger.warn('[package:version:syncPackageVersionFiles] check lock fail');
this.logger.warn('[package:version:syncPackageVersionFiles] check lock:%s fail', lockName);
throw new ConflictError('Package version file sync is currently in progress. Please try again later.');
}
}
}

async #updateUnpkgWhiteList() {
if (!this.config.cnpmcore.enableSyncUnpkgFilesWhiteList) return;
const whiteListScope = '';
const whiteListPackageName = 'unpkg-white-list';
const whiteListPackageVersion = await this.packageVersionRepository.findVersionByTag(
whiteListScope, whiteListPackageName, 'latest');
if (!whiteListPackageVersion) return;
// same version, skip update for performance
if (this.#unpkgWhiteListCurrentVersion === whiteListPackageVersion) return;

// update the new version white list
const { manifest } = await this.packageManagerService.showPackageVersionManifest(
whiteListScope, whiteListPackageName, whiteListPackageVersion, false, true);
if (!manifest) return;
this.#unpkgWhiteListCurrentVersion = manifest.version;
this.#unpkgWhiteListAllowPackages = manifest.allowPackages ?? {} as any;
this.#unpkgWhiteListAllowScopes = manifest.allowScopes ?? [] as any;
this.logger.info('[PackageVersionFileService.updateUnpkgWhiteList] version:%s, total %s packages, %s scopes',
whiteListPackageVersion,
Object.keys(this.#unpkgWhiteListAllowPackages).length,
this.#unpkgWhiteListAllowScopes.length,
);
}

async #checkPackageVersionInUnpkgWhiteList(pkgScope: string, pkgName: string, pkgVersion: string) {
if (!this.config.cnpmcore.enableSyncUnpkgFilesWhiteList) return;
await this.#updateUnpkgWhiteList();

// check allow scopes
if (this.#unpkgWhiteListAllowScopes.includes(pkgScope)) return;

// check allow packages
const fullname = getFullname(pkgScope, pkgName);
const pkgConfig = this.#unpkgWhiteListAllowPackages[fullname];
if (!pkgConfig) {
throw new ForbiddenError(`"${fullname}" is not allow to unpkg files, see ${unpkgWhiteListUrl}`);
}
if (!pkgConfig.version || !semver.satisfies(pkgVersion, pkgConfig.version)) {
throw new ForbiddenError(`"${fullname}@${pkgVersion}" not satisfies "${pkgConfig.version}" to unpkg files, see ${unpkgWhiteListUrl}`);
}
}

// 基于 latest version 同步 package readme
Expand Down Expand Up @@ -113,8 +168,16 @@ export class PackageVersionFileService extends AbstractService {

async syncPackageVersionFiles(pkgVersion: PackageVersion) {
const files: PackageVersionFile[] = [];
// must set enableUnpkg and enableSyncUnpkgFiles = true both
if (!this.config.cnpmcore.enableUnpkg) return files;
if (!this.config.cnpmcore.enableSyncUnpkgFiles) return files;

const pkg = await this.packageRepository.findPackageByPackageId(pkgVersion.packageId);
if (!pkg) return files;

// check unpkg white list
await this.#checkPackageVersionInUnpkgWhiteList(pkg.scope, pkg.name, pkgVersion.version);

const dirname = `unpkg_${pkg.fullname.replace('/', '_')}@${pkgVersion.version}_${randomUUID()}`;
const tmpdir = await createTempDir(this.config.dataDir, dirname);
const tarFile = `${tmpdir}.tgz`;
Expand Down
4 changes: 4 additions & 0 deletions app/port/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export type CnpmcoreConfig = {
* enable sync unpkg files
*/
enableSyncUnpkgFiles: boolean;
/**
* enable sync unpkg files from the white list, https://github.com/cnpm/unpkg-white-list
*/
enableSyncUnpkgFilesWhiteList: boolean;
/**
* enable this would make sync specific version task not append latest version into this task automatically,it would mark the local latest stable version as latest tag.
* in most cases, you should set to false to keep the same behavior as source registry.
Expand Down
3 changes: 0 additions & 3 deletions app/port/controller/PackageVersionFileController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,9 @@ export class PackageVersionFileController extends AbstractController {

if (!file) {
const possibleFile = await this.#searchPossibleEntries(packageVersion, path);

if (possibleFile) {
const route = `/${fullname}/${versionSpec}/files${possibleFile.path}${hasMeta ? '?meta' : ''}`;

ctx.redirect(route);

return;
}

Expand Down
3 changes: 2 additions & 1 deletion app/port/controller/package/SavePackageVersionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export class SavePackageVersionController extends AbstractController {
const registry = await this.registryManagerService.ensureSelfRegistry();

let packageVersionEntity: PackageVersionEntity | undefined;
const lockName = `${pkg.name}:publish`;
const lockRes = await this.cacheAdapter.usingLock(`${pkg.name}:publish`, 60, async () => {
packageVersionEntity = await this.packageManagerService.publish({
scope,
Expand All @@ -240,7 +241,7 @@ export class SavePackageVersionController extends AbstractController {

// lock fail
if (!lockRes) {
this.logger.warn('[package:version:add] check lock fail');
this.logger.warn('[package:version:add] check lock:%s fail', lockName);
throw new ConflictError('Unable to create the publication lock, please try again later.');
}

Expand Down
1 change: 1 addition & 0 deletions config/config.default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const cnpmcoreConfig: CnpmcoreConfig = {
redirectNotFound: true,
enableUnpkg: true,
enableSyncUnpkgFiles: true,
enableSyncUnpkgFilesWhiteList: false,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

默认关闭

strictSyncSpecivicVersion: false,
enableElasticsearch: !!process.env.CNPMCORE_CONFIG_ENABLE_ES,
elasticsearchIndex: 'cnpmcore_packages',
Expand Down
Loading
Loading