diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 091c36d4f28..bd24587d0a7 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -32,6 +32,7 @@ import { SharedProcessEnvironmentService } from 'vs/platform/sharedProcess/node/ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementChannel, ExtensionTipsChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/electron-sandbox/extensionTipsService'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -331,6 +332,7 @@ class SharedProcessMain extends Disposable { // Extension Management services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); // Extension Gallery diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 14dbbbb1783..6c65566f766 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -24,6 +24,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; @@ -181,6 +182,7 @@ class CliMain extends Disposable { // Extensions services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService, undefined, true)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService, undefined, true)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService, undefined, true)); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 3953e06a708..f41968d9b0e 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -142,7 +142,7 @@ export interface INativeEnvironmentService extends IEnvironmentService { // --- extensions extensionsPath: string; - extensionsDownloadPath: string; + extensionsDownloadLocation: URI; builtinExtensionsPath: string; // --- use keytar for credentials diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 934a8657683..17c18da90b4 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -129,13 +129,13 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions')); } - get extensionsDownloadPath(): string { + get extensionsDownloadLocation(): URI { const cliExtensionsDownloadDir = this.args['extensions-download-dir']; if (cliExtensionsDownloadDir) { - return resolve(cliExtensionsDownloadDir); + return URI.file(resolve(cliExtensionsDownloadDir)); } - return join(this.userDataPath, 'CachedExtensionVSIXs'); + return URI.file(join(this.userDataPath, 'CachedExtensionVSIXs')); } @memoize diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 3e91ecf3b2c..52696de6e81 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -28,6 +28,7 @@ export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; readonly operation: InstallOperation; + readonly wasVerified?: boolean; run(): Promise<{ local: ILocalExtension; metadata: Metadata }>; waitUntilTaskIsFinished(): Promise<{ local: ILocalExtension; metadata: Metadata }>; cancel(): void; @@ -243,6 +244,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000; reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), + wasVerified: task.wasVerified, duration: new Date().getTime() - startTime, durationSinceUpdate }); @@ -256,7 +258,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source, context: options.context, profileLocation: options.profileLocation, applicationScoped: local.isApplicationScoped }); } catch (error) { if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { extensionData: getGalleryExtensionTelemetryData(task.source), duration: new Date().getTime() - startTime, error }); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', { + extensionData: getGalleryExtensionTelemetryData(task.source), + wasVerified: task.wasVerified, + duration: new Date().getTime() - startTime, + error + }); } this.logService.error('Error while installing the extension:', task.identifier.id); throw error; @@ -665,6 +672,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract getManifest(vsix: URI): Promise; abstract install(vsix: URI, options?: InstallVSIXOptions): Promise; abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + abstract download(extension: IGalleryExtension, operation: InstallOperation): Promise; abstract getMetadata(extension: ILocalExtension): Promise; abstract updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise; @@ -693,8 +701,22 @@ function toExtensionManagementError(error: Error): ExtensionManagementError { return e; } -export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, duration, error, durationSinceUpdate }: { extensionData: any; duration?: number; durationSinceUpdate?: number; error?: Error }): void { - const errorcode = error ? error instanceof ExtensionManagementError ? error.code : ExtensionManagementErrorCode.Internal : undefined; +export function reportTelemetry(telemetryService: ITelemetryService, eventName: string, { extensionData, wasVerified, duration, error, durationSinceUpdate }: { extensionData: any; wasVerified?: boolean; duration?: number; durationSinceUpdate?: number; error?: Error }): void { + let errorcode: ExtensionManagementErrorCode | undefined; + let errorcodeDetail: string | undefined; + + if (error) { + if (error instanceof ExtensionManagementError) { + errorcode = error.code; + + if (error.code === ExtensionManagementErrorCode.Signature) { + errorcodeDetail = error.message; + } + } else { + errorcode = ExtensionManagementErrorCode.Internal; + } + } + /* __GDPR__ "extensionGallery:install" : { "owner": "sandy081", @@ -702,7 +724,9 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName: "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "durationSinceUpdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "recommendationReason": { "retiredFromVersion": "1.23.0", "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] @@ -725,12 +749,14 @@ export function reportTelemetry(telemetryService: ITelemetryService, eventName: "success": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "duration" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "errorcode": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errorcodeDetail": { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "wasVerified" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData}" ] } */ - telemetryService.publicLog(eventName, { ...extensionData, success: !error, duration, errorcode, durationSinceUpdate }); + telemetryService.publicLog(eventName, { ...extensionData, wasVerified, success: !error, duration, errorcode, errorcodeDetail, durationSinceUpdate }); } export abstract class AbstractExtensionTask { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index e9a3651cb50..c5fbb1835ab 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -193,7 +193,8 @@ const AssetType = { Manifest: 'Microsoft.VisualStudio.Code.Manifest', VSIX: 'Microsoft.VisualStudio.Services.VSIXPackage', License: 'Microsoft.VisualStudio.Services.Content.License', - Repository: 'Microsoft.VisualStudio.Services.Links.Source' + Repository: 'Microsoft.VisualStudio.Services.Links.Source', + Signature: 'Microsoft.VisualStudio.Services.VsixSignature' }; const PropertyType = { @@ -505,6 +506,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller repository: getRepositoryAsset(version), download: getDownloadAsset(version), icon: getVersionAsset(version, AssetType.Icon), + signature: getVersionAsset(version, AssetType.Signature), coreTranslations: getCoreTranslationAssets(version) }; @@ -542,6 +544,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller hasPreReleaseVersion: isPreReleaseVersion(latestVersion), hasReleaseVersion: true, preview: getIsPreview(galleryExtension.flags), + isSigned: !!assets.signature }; } @@ -1027,6 +1030,17 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi log(new Date().getTime() - startTime); } + async downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise { + if (!extension.assets.signature) { + throw new Error('No signature asset found'); + } + + this.logService.trace('ExtensionGalleryService#downloadSignatureArchive', extension.identifier.id); + + const context = await this.getAsset(extension.assets.signature); + await this.fileService.writeFile(location, context.stream); + } + async getReadme(extension: IGalleryExtension, token: CancellationToken): Promise { if (extension.assets.readme) { const context = await this.getAsset(extension.assets.readme, {}, token); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 7ab51f1d2cd..ba3a2b083ce 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -178,6 +178,7 @@ export interface IGalleryExtensionAssets { repository: IGalleryExtensionAsset | null; download: IGalleryExtensionAsset; icon: IGalleryExtensionAsset | null; + signature: IGalleryExtensionAsset | null; coreTranslations: [string, IGalleryExtensionAsset][]; } @@ -224,6 +225,7 @@ export interface IGalleryExtension { preview: boolean; hasPreReleaseVersion: boolean; hasReleaseVersion: boolean; + isSigned: boolean; allTargetPlatforms: TargetPlatform[]; assets: IGalleryExtensionAssets; properties: IGalleryExtensionProperties; @@ -335,6 +337,7 @@ export interface IExtensionGalleryService { getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; + downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise; getReadme(extension: IGalleryExtension, token: CancellationToken): Promise; getManifest(extension: IGalleryExtension, token: CancellationToken): Promise; @@ -390,6 +393,7 @@ export enum ExtensionManagementErrorCode { CorruptZip = 'CorruptZip', IncompleteZip = 'IncompleteZip', Internal = 'Internal', + Signature = 'Signature' } export class ExtensionManagementError extends Error { @@ -440,6 +444,7 @@ export interface IExtensionManagementService { getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; getExtensionsControlManifest(): Promise; + download(extension: IGalleryExtension, operation: InstallOperation): Promise; getMetadata(extension: ILocalExtension): Promise; updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise; updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 0f59245f809..f23ffa0f27c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -10,7 +10,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI { @@ -75,6 +75,7 @@ export class ExtensionManagementChannel implements IServerChannel { case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer)); case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer)); case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest(); + case 'download': return this.service.download(args[0], args[1]); } throw new Error('Invalid call'); @@ -177,6 +178,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('getExtensionsControlManifest')); } + async download(extension: IGalleryExtension, operation: InstallOperation): Promise { + const result = await this.channel.call('download', [extension, operation]); + return URI.revive(result); + } + registerParticipant() { throw new Error('Not Supported'); } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 2b143f85177..8fbe4ebff24 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -125,6 +125,7 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any "publisherDisplayName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "isPreReleaseVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "dependencies": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "isSigned": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ "${GalleryExtensionTelemetryData2}" ] @@ -140,6 +141,7 @@ export function getGalleryExtensionTelemetryData(extension: IGalleryExtension): publisherDisplayName: extension.publisherDisplayName, isPreReleaseVersion: extension.properties.isPreReleaseVersion, dependencies: !!(extension.properties.dependencies && extension.properties.dependencies.length > 0), + isSigned: extension.isSigned, ...extension.telemetryData }; } diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index 3077d4586f8..da63253da47 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -6,20 +6,25 @@ import { Promises } from 'vs/base/common/async'; import { getErrorMessage } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises as FSPromises } from 'vs/base/node/pfs'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; export class ExtensionsDownloader extends Disposable { + private static readonly SignatureArchiveExtension = '.sigzip'; + readonly extensionsDownloadDir: URI; private readonly cache: number; private readonly cleanUpPromise: Promise; @@ -28,47 +33,83 @@ export class ExtensionsDownloader extends Disposable { @INativeEnvironmentService environmentService: INativeEnvironmentService, @IFileService private readonly fileService: IFileService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService, @ILogService private readonly logService: ILogService, ) { super(); - this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath); - this.cache = 20; // Cache 20 downloads + this.extensionsDownloadDir = environmentService.extensionsDownloadLocation; + this.cache = 20; // Cache 20 downloaded VSIX files this.cleanUpPromise = this.cleanUp(); } - async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise { + async download(extension: IGalleryExtension, operation: InstallOperation): Promise<{ readonly location: URI; verified: boolean }> { await this.cleanUpPromise; - const vsixName = this.getName(extension); - const location = joinPath(this.extensionsDownloadDir, vsixName); - - // Download only if vsix does not exist - if (!await this.fileService.exists(location)) { - // Download to temporary location first only if vsix does not exist - const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); - if (!await this.fileService.exists(tempLocation)) { - await this.extensionGalleryService.download(extension, tempLocation, operation); - } - - try { - // Rename temp location to original - await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); - } catch (error) { - try { - await this.fileService.del(tempLocation); - } catch (e) { /* ignore */ } - if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because vsix was downloaded by another source. So ignoring renaming.`, extension.identifier.id); - } else { - this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the vsix from downloaded location`, tempLocation.path); - throw error; - } - } + const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); + try { + await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation)); + } catch (error) { + throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download); } + let verified: boolean = false; + if (extension.isSigned && this.configurationService.getValue('extensions.verifySignature') === true) { + const signatureArchiveLocation = await this.downloadSignatureArchive(extension); + try { + verified = await this.extensionSignatureVerificationService.verify(location.fsPath, signatureArchiveLocation.fsPath); + } catch (error) { + await this.delete(signatureArchiveLocation); + await this.delete(location); + throw new ExtensionManagementError((error as ExtensionSignatureVerificationError).code, ExtensionManagementErrorCode.Signature); + } + } + + return { location, verified }; + } + + private async downloadSignatureArchive(extension: IGalleryExtension): Promise { + await this.cleanUpPromise; + + const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`); + await this.downloadFile(extension, location, location => this.extensionGalleryService.downloadSignatureArchive(extension, location)); return location; } + private async downloadFile(extension: IGalleryExtension, location: URI, downloadFn: (location: URI) => Promise): Promise { + // Do not download if exists + if (await this.fileService.exists(location)) { + return; + } + + // Download directly if locaiton is not file scheme + if (location.scheme !== Schemas.file) { + await downloadFn(location); + return; + } + + // Download to temporary location first only if file does not exist + const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`); + if (!await this.fileService.exists(tempLocation)) { + await downloadFn(tempLocation); + } + + try { + // Rename temp location to original + await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); + } catch (error) { + try { + await this.fileService.del(tempLocation); + } catch (e) { /* ignore */ } + if (error.code === 'ENOTEMPTY') { + this.logService.info(`Rename failed because the file was downloaded by another source. So ignoring renaming.`, extension.identifier.id, location.path); + } else { + this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted the file from downloaded location`, tempLocation.path); + throw error; + } + } + } + async delete(location: URI): Promise { await this.cleanUpPromise; await this.fileService.del(location); @@ -89,20 +130,27 @@ export class ExtensionsDownloader extends Disposable { private async cleanUp(): Promise { try { if (!(await this.fileService.exists(this.extensionsDownloadDir))) { - this.logService.trace('Extension VSIX downlads cache dir does not exist'); + this.logService.trace('Extension VSIX downloads cache dir does not exist'); return; } const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true }); if (folderStat.children) { const toDelete: URI[] = []; - const all: [ExtensionKey, IFileStatWithMetadata][] = []; + const vsixs: [ExtensionKey, IFileStatWithMetadata][] = []; + const signatureArchives: URI[] = []; + for (const stat of folderStat.children) { - const extension = ExtensionKey.parse(stat.name); - if (extension) { - all.push([extension, stat]); + if (stat.name.endsWith(ExtensionsDownloader.SignatureArchiveExtension)) { + signatureArchives.push(stat.resource); + } else { + const extension = ExtensionKey.parse(stat.name); + if (extension) { + vsixs.push([extension, stat]); + } } } - const byExtension = groupByExtension(all, ([extension]) => extension); + + const byExtension = groupByExtension(vsixs, ([extension]) => extension); const distinct: IFileStatWithMetadata[] = []; for (const p of byExtension) { p.sort((a, b) => semver.rcompare(a[0].version, b[0].version)); @@ -111,8 +159,10 @@ export class ExtensionsDownloader extends Disposable { } distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest + toDelete.push(...signatureArchives); // Delete all signature archives + await Promises.settled(toDelete.map(resource => { - this.logService.trace('Deleting vsix from cache', resource.path); + this.logService.trace('Deleting from cache', resource.path); return this.fileService.del(resource); })); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e8a8e731abb..520c5667aae 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -78,7 +78,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IFileService private readonly fileService: IFileService, @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService, + @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService ) { super(galleryService, telemetryService, logService, productService, userDataProfilesService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); @@ -177,6 +177,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi return this.extensionsScanner.cleanUp(removeOutdated); } + async download(extension: IGalleryExtension, operation: InstallOperation): Promise { + const { location } = await this.extensionsDownloader.download(extension, operation); + return location; + } + private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise }> { if (vsix.scheme === Schemas.file) { return { location: vsix, async cleanup() { } }; @@ -247,7 +252,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } -class ExtensionsScanner extends Disposable { +export class ExtensionsScanner extends Disposable { private readonly uninstalledPath: string; private readonly uninstalledFileLimiter: Queue; @@ -463,6 +468,7 @@ class ExtensionsScanner extends Disposable { updated: !!extension.metadata?.updated, }; } + private async removeUninstalledExtensions(): Promise { const uninstalled = await this.getUninstalledExtensions(); const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions @@ -528,6 +534,8 @@ class ExtensionsScanner extends Disposable { abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILocalExtension; metadata: Metadata }> implements IInstallExtensionTask { + public wasVerified: boolean = false; + protected _operation = InstallOperation.Install; get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } @@ -584,7 +592,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask<{ local: ILoca } -class InstallGalleryExtensionTask extends InstallExtensionTask { +export class InstallGalleryExtensionTask extends InstallExtensionTask { constructor( private readonly manifest: IExtensionManifest, @@ -628,46 +636,34 @@ class InstallGalleryExtensionTask extends InstallExtensionTask { return { local, metadata }; } - const zipPath = await this.downloadExtension(this.gallery, this._operation); + const { location, verified } = await this.extensionsDownloader.download(this.gallery, this._operation); try { - const local = await this.installExtension({ zipPath, key: ExtensionKey.create(this.gallery), metadata }, token); + this.wasVerified = !!verified; + this.validateManifest(location.fsPath); + const local = await this.installExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, token); if (existingExtension && !this.options.profileLocation && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) { await this.extensionsScanner.setUninstalled(existingExtension); } return { local, metadata }; } catch (error) { - await this.deleteDownloadedVSIX(zipPath); + try { + await this.extensionsDownloader.delete(location); + } catch (error) { + /* Ignore */ + this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error)); + } throw error; } } - private async deleteDownloadedVSIX(vsix: string): Promise { - try { - await this.extensionsDownloader.delete(URI.file(vsix)); - } catch (error) { - /* Ignore */ - this.logService.warn('Error while deleting the downloaded vsix', vsix.toString(), getErrorMessage(error)); - } - } - - private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise { - let zipPath: string | undefined; - try { - this.logService.trace('Started downloading extension:', extension.identifier.id); - zipPath = (await this.extensionsDownloader.downloadExtension(extension, operation)).fsPath; - this.logService.info('Downloaded extension:', extension.identifier.id, zipPath); - } catch (error) { - throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Download); - } - + protected async validateManifest(zipPath: string): Promise { try { await getManifest(zipPath); - return zipPath; } catch (error) { - await this.deleteDownloadedVSIX(zipPath); throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid); } } + } class InstallVSIXTask extends InstallExtensionTask { diff --git a/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts new file mode 100644 index 00000000000..bf5f06c4f0a --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionSignatureVerificationService.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IExtensionSignatureVerificationService = createDecorator('IExtensionSignatureVerificationService'); + +/** + * A service for verifying signed extensions. + */ +export interface IExtensionSignatureVerificationService { + readonly _serviceBrand: undefined; + + /** + * Verifies an extension file (.vsix) against a signature archive file. + * @param { string } vsixFilePath The extension file path. + * @param { string } signatureArchiveFilePath The signature archive file path. + * @returns { Promise } A promise with `true` if the extension is validly signed and trusted; + * otherwise, `false` because verification is not enabled (e.g.: in the OSS version of VS Code). + * @throws { ExtensionVerificationError } An error with a code indicating the validity, integrity, or trust issue + * found during verification or a more fundamental issue (e.g.: a required dependency was not found). + */ + verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise; +} + +declare module vsceSign { + export function verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise; +} + +/** + * An error raised during extension signature verification. + */ +export interface ExtensionSignatureVerificationError extends Error { + readonly code: string; +} + +export class ExtensionSignatureVerificationService implements IExtensionSignatureVerificationService { + declare readonly _serviceBrand: undefined; + + private moduleLoadingPromise: Promise | undefined; + + private vsceSign(): Promise { + if (!this.moduleLoadingPromise) { + this.moduleLoadingPromise = new Promise( + (resolve, reject) => require( + ['vsce-sign'], + async (obj) => { + const instance = obj; + + return resolve(instance); + }, reject)); + } + + return this.moduleLoadingPromise; + } + + public async verify(vsixFilePath: string, signatureArchiveFilePath: string): Promise { + let module: typeof vsceSign; + + try { + module = await this.vsceSign(); + } catch (error) { + return false; + } + + return module.verify(vsixFilePath, signatureArchiveFilePath); + } +} diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts new file mode 100644 index 00000000000..6d9db54a3a5 --- /dev/null +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { platform } from 'vs/base/common/platform'; +import { arch } from 'vs/base/common/process'; +import { joinPath } from 'vs/base/common/resources'; +import { isBoolean } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { mock } from 'vs/base/test/common/mock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { ExtensionsScanner, InstallGalleryExtensionTask } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILogService, NullLogService } from 'vs/platform/log/common/log'; + +const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); + +class TestExtensionsScanner extends mock() { + override async scanExtensions(): Promise { return []; } +} + +class TestExtensionSignatureVerificationService extends mock() { + + constructor(private readonly verificationResult: string | boolean) { + super(); + } + + override async verify(): Promise { + if (isBoolean(this.verificationResult)) { + return this.verificationResult; + } + const error = Error(this.verificationResult); + (error as any).code = this.verificationResult; + throw error; + } +} + +class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { + + installed = false; + + constructor( + extension: IGalleryExtension, + extensionDownloader: ExtensionsDownloader, + ) { + super( + { + name: extension.name, + publisher: extension.publisher, + version: extension.version, + engines: { vscode: '*' }, + }, + extension, + {}, + extensionDownloader, + new TestExtensionsScanner(), + new NullLogService(), + ); + } + + protected override async installExtension(): Promise { + this.installed = true; + return new class extends mock() { }; + } + + protected override async validateManifest(): Promise { } +} + +suite('InstallGalleryExtensionTask Tests', () => { + + const disposables = new DisposableStore(); + + teardown(() => disposables.clear()); + + test('if verification is disabled by default, the task skips verification', async () => { + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader('error')); + + await testObject.run(); + + assert.strictEqual(testObject.wasVerified, false); + assert.strictEqual(testObject.installed, true); + }); + + test('if verification is disabled by setting set to false, the task skips verification', async () => { + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader('error', false)); + + await testObject.run(); + + assert.strictEqual(testObject.wasVerified, false); + assert.strictEqual(testObject.installed, true); + }); + + test('if verification is disabled because the module is not loaded, the task skips verification', async () => { + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader(false, true)); + + await testObject.run(); + + assert.strictEqual(testObject.wasVerified, false); + assert.strictEqual(testObject.installed, true); + }); + + test('if verification fails, the task throws', async () => { + const errorCode = 'IntegrityCheckFailed'; + + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader(errorCode, true)); + + try { + await testObject.run(); + } catch (e) { + assert.ok(e instanceof ExtensionManagementError); + assert.strictEqual(e.code, ExtensionManagementErrorCode.Signature); + assert.strictEqual(e.message, errorCode); + assert.strictEqual(testObject.wasVerified, false); + assert.strictEqual(testObject.installed, false); + return; + } + + assert.fail('It should have thrown.'); + + }); + + test('if verification succeeds, the task completes', async () => { + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: true }), anExtensionsDownloader(true, true)); + + await testObject.run(); + + assert.strictEqual(testObject.wasVerified, true); + assert.strictEqual(testObject.installed, true); + }); + + test('task completes for unsigned extension', async () => { + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader(true, true)); + + await testObject.run(); + + assert.strictEqual(testObject.wasVerified, false); + assert.strictEqual(testObject.installed, true); + }); + + test('task completes for an unsigned extension even when signature verification throws error', async () => { + const testObject = new TestInstallGalleryExtensionTask(aGalleryExtension('a', { isSigned: false }), anExtensionsDownloader('error', true)); + + await testObject.run(); + + assert.strictEqual(testObject.wasVerified, false); + assert.strictEqual(testObject.installed, true); + }); + + function anExtensionsDownloader(verificationResult: string | boolean, isSignatureVerificationEnabled?: boolean): ExtensionsDownloader { + const logService = new NullLogService(); + const fileService = disposables.add(new FileService(logService)); + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(ROOT.scheme, fileSystemProvider); + + const instantiationService = new TestInstantiationService(); + instantiationService.stub(IFileService, fileService); + instantiationService.stub(ILogService, logService); + instantiationService.stub(INativeEnvironmentService, >{ extensionsDownloadLocation: joinPath(ROOT, 'CachedExtensionVSIXs') }); + instantiationService.stub(IExtensionGalleryService, >{ + async download(extension, location, operation) { + await fileService.writeFile(location, VSBuffer.fromString('extension vsix')); + }, + async downloadSignatureArchive(extension, location) { + await fileService.writeFile(location, VSBuffer.fromString('extension signature')); + }, + }); + instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(isSignatureVerificationEnabled) ? { extensions: { verifySignature: isSignatureVerificationEnabled } } : undefined)); + instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(verificationResult)); + return instantiationService.createInstance(ExtensionsDownloader); + } + + function aGalleryExtension(name: string, properties: Partial = {}, galleryExtensionProperties: any = {}, assets: Partial = {}): IGalleryExtension { + const targetPlatform = getTargetPlatform(platform, arch); + const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); + galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; + galleryExtension.assets = { ...galleryExtension.assets, ...assets }; + galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; + return galleryExtension; + } +}); diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 086f7dec035..0b4cab3b1cd 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -15,6 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionGalleryService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/product/common/product'; @@ -110,6 +111,7 @@ class CliMain extends Disposable { services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(ILanguagePackService, new SyncDescriptor(NativeLanguagePackService)); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index c2517797d58..f7a5dabd6f6 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -25,6 +25,7 @@ import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMai import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService'; import { ExtensionManagementCLI } from 'vs/platform/extensionManagement/common/extensionManagementCLI'; import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionManagementService, INativeServerExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; @@ -172,6 +173,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService)); services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService)); + services.set(IExtensionSignatureVerificationService, new SyncDescriptor(ExtensionSignatureVerificationService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); const instantiationService: IInstantiationService = new InstantiationService(services); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index b9801ecbecb..7da15b1307b 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -85,6 +85,7 @@ const mockExtensionGallery: IGalleryExtension[] = [ icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }), aGalleryExtension('MockExtension2', { @@ -107,6 +108,7 @@ const mockExtensionGallery: IGalleryExtension[] = [ icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }) ]; @@ -166,6 +168,7 @@ const noAssets: IGalleryExtensionAssets = { manifest: null, readme: null, repository: null, + signature: null, coreTranslations: [] }; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index d3fa2255b1b..2eabf49b50f 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -164,6 +164,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }); @@ -314,6 +315,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { icon: { uri: 'uri:icon', fallbackUri: 'fallback:icon' }, license: { uri: 'uri:license', fallbackUri: 'fallback:license' }, repository: { uri: 'uri:repository', fallbackUri: 'fallback:repository' }, + signature: { uri: 'uri:signature', fallbackUri: 'fallback:signature' }, coreTranslations: [] }); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local1, local2]); @@ -1440,6 +1442,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { manifest: null, readme: null, repository: null, + signature: null, coreTranslations: [] }; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index a8d11483188..5f76f7e51f8 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,7 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -188,6 +188,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench .map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier); } + download(extension: IGalleryExtension, operation: InstallOperation): Promise { + if (this.extensionManagementServerService.localExtensionManagementServer) { + return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.download(extension, operation); + } + throw new Error('Cannot download extension'); + } + async install(vsix: URI, options?: InstallVSIXOptions): Promise { const manifest = await this.getManifest(vsix); return this.installVSIX(vsix, manifest, options); diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 47c2436709c..79333395143 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -147,6 +147,7 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe unzip(zipLocation: URI): Promise { throw new Error('unsupported'); } getManifest(vsix: URI): Promise { throw new Error('unsupported'); } updateExtensionScope(): Promise { throw new Error('unsupported'); } + download(): Promise { throw new Error('unsupported'); } private async whenProfileChanged(e: DidChangeUserDataProfileEvent): Promise { const previousProfileLocation = e.previous.extensionsResource; diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index 3d2c557eced..da1f95ec42e 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -92,7 +92,7 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel return { location: vsix, async cleanup() { } }; } this.logService.trace('Downloading extension from', vsix.toString()); - const location = joinPath(URI.file(this.nativeEnvironmentService.extensionsDownloadPath), generateUuid()); + const location = joinPath(this.nativeEnvironmentService.extensionsDownloadLocation, generateUuid()); await this.downloadService.download(vsix, location); this.logService.info('Downloaded extension to', location.toString()); const cleanup = async () => { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 6320906637a..44bc797e9ab 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -16,10 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { generateUuid } from 'vs/base/common/uuid'; -import { joinPath } from 'vs/base/common/resources'; import { IExtensionManagementServer, IProfileAwareExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { Promises } from 'vs/base/common/async'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -40,7 +37,6 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService private readonly productService: IProductService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { @@ -120,9 +116,8 @@ export class NativeRemoteExtensionManagementService extends ExtensionManagementC private async downloadCompatibleAndInstall(extension: IGalleryExtension, installed: ILocalExtension[], installOptions: InstallOptions): Promise { const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion); - const location = joinPath(URI.file(this.environmentService.extensionsDownloadPath), generateUuid()); this.logService.trace('Downloading extension:', compatible.identifier.id); - await this.galleryService.download(compatible, location, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install); + const location = await this.localExtensionManagementServer.extensionManagementService.download(compatible, installed.filter(i => areSameExtensions(i.identifier, compatible.identifier))[0] ? InstallOperation.Update : InstallOperation.Install); this.logService.info('Downloaded extension:', compatible.identifier.id, location.path); try { const local = await super.install(location, installOptions); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 796e5fc4fe3..45eeada71ea 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -2004,6 +2004,9 @@ export class TestWorkbenchExtensionManagementService implements IWorkbenchExtens async updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise { return local; } registerParticipant(pariticipant: IExtensionManagementParticipant): void { } async getTargetPlatform(): Promise { return TargetPlatform.UNDEFINED; } + download(): Promise { + throw new Error('Method not implemented.'); + } } export class TestUserDataProfileService implements IUserDataProfileService {