Skip to content

Commit 3b49195

Browse files
committed
- add download api to ext mgmt service
- support verifying signed extension in remote when downloading locally
1 parent 083edcc commit 3b49195

13 files changed

+123
-89
lines changed

src/vs/platform/environment/common/environment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export interface INativeEnvironmentService extends IEnvironmentService {
142142

143143
// --- extensions
144144
extensionsPath: string;
145-
extensionsDownloadPath: string;
145+
extensionsDownloadLocation: URI;
146146
builtinExtensionsPath: string;
147147

148148
// --- use keytar for credentials

src/vs/platform/environment/common/environmentService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron
127127
return normalize(join(FileAccess.asFileUri('', require).fsPath, '..', 'extensions'));
128128
}
129129

130-
get extensionsDownloadPath(): string {
130+
get extensionsDownloadLocation(): URI {
131131
const cliExtensionsDownloadDir = this.args['extensions-download-dir'];
132132
if (cliExtensionsDownloadDir) {
133-
return resolve(cliExtensionsDownloadDir);
133+
return URI.file(resolve(cliExtensionsDownloadDir));
134134
}
135135

136-
return join(this.userDataPath, 'CachedExtensionVSIXs');
136+
return URI.file(join(this.userDataPath, 'CachedExtensionVSIXs'));
137137
}
138138

139139
@memoize

src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
672672
abstract getManifest(vsix: URI): Promise<IExtensionManifest>;
673673
abstract install(vsix: URI, options?: InstallVSIXOptions): Promise<ILocalExtension>;
674674
abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]>;
675+
abstract download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI>;
675676

676677
abstract getMetadata(extension: ILocalExtension): Promise<Metadata | undefined>;
677678
abstract updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;

src/vs/platform/extensionManagement/common/extensionManagement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ export interface IExtensionManagementService {
444444
getInstalled(type?: ExtensionType, profileLocation?: URI): Promise<ILocalExtension[]>;
445445
getExtensionsControlManifest(): Promise<IExtensionsControlManifest>;
446446

447+
download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI>;
447448
getMetadata(extension: ILocalExtension): Promise<Metadata | undefined>;
448449
updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension>;
449450
updateExtensionScope(local: ILocalExtension, isMachineScoped: boolean): Promise<ILocalExtension>;

src/vs/platform/extensionManagement/common/extensionManagementIpc.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { cloneAndChange } from 'vs/base/common/objects';
1010
import { URI, UriComponents } from 'vs/base/common/uri';
1111
import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
1212
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
13-
import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
13+
import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
1414
import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions';
1515

1616
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
@@ -75,6 +75,7 @@ export class ExtensionManagementChannel implements IServerChannel {
7575
case 'updateMetadata': return this.service.updateMetadata(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
7676
case 'updateExtensionScope': return this.service.updateExtensionScope(transformIncomingExtension(args[0], uriTransformer), args[1]).then(e => transformOutgoingExtension(e, uriTransformer));
7777
case 'getExtensionsControlManifest': return this.service.getExtensionsControlManifest();
78+
case 'download': return this.service.download(args[0], args[1]);
7879
}
7980

8081
throw new Error('Invalid call');
@@ -177,6 +178,11 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt
177178
return Promise.resolve(this.channel.call<IExtensionsControlManifest>('getExtensionsControlManifest'));
178179
}
179180

181+
async download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
182+
const result = await this.channel.call<UriComponents>('download', [extension, operation]);
183+
return URI.revive(result);
184+
}
185+
180186
registerParticipant() { throw new Error('Not Supported'); }
181187
}
182188

src/vs/platform/extensionManagement/node/extensionDownloader.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66
import { Promises } from 'vs/base/common/async';
77
import { getErrorMessage } from 'vs/base/common/errors';
88
import { Disposable } from 'vs/base/common/lifecycle';
9+
import { Schemas } from 'vs/base/common/network';
910
import { isWindows } from 'vs/base/common/platform';
1011
import { joinPath } from 'vs/base/common/resources';
1112
import * as semver from 'vs/base/common/semver/semver';
1213
import { URI } from 'vs/base/common/uri';
1314
import { generateUuid } from 'vs/base/common/uuid';
1415
import { Promises as FSPromises } from 'vs/base/node/pfs';
16+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1517
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
16-
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
18+
import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
1719
import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
20+
import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
1821
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
1922
import { ILogService } from 'vs/platform/log/common/log';
2023

@@ -30,23 +33,42 @@ export class ExtensionsDownloader extends Disposable {
3033
@INativeEnvironmentService environmentService: INativeEnvironmentService,
3134
@IFileService private readonly fileService: IFileService,
3235
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
36+
@IConfigurationService private readonly configurationService: IConfigurationService,
37+
@IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService,
3338
@ILogService private readonly logService: ILogService,
3439
) {
3540
super();
36-
this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath);
41+
this.extensionsDownloadDir = environmentService.extensionsDownloadLocation;
3742
this.cache = 20; // Cache 20 downloaded VSIX files
3843
this.cleanUpPromise = this.cleanUp();
3944
}
4045

41-
async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
46+
async download(extension: IGalleryExtension, operation: InstallOperation): Promise<{ readonly location: URI; verified: boolean }> {
4247
await this.cleanUpPromise;
4348

4449
const location = joinPath(this.extensionsDownloadDir, this.getName(extension));
45-
await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation));
46-
return location;
50+
try {
51+
await this.downloadFile(extension, location, location => this.extensionGalleryService.download(extension, location, operation));
52+
} catch (error) {
53+
throw new ExtensionManagementError(error.message, ExtensionManagementErrorCode.Download);
54+
}
55+
56+
let verified: boolean = false;
57+
if (extension.isSigned && this.configurationService.getValue('extensions.verifySignature') === true) {
58+
const signatureArchiveLocation = await this.downloadSignatureArchive(extension);
59+
try {
60+
verified = await this.extensionSignatureVerificationService.verify(location.fsPath, signatureArchiveLocation.fsPath);
61+
} catch (error) {
62+
await this.delete(signatureArchiveLocation);
63+
await this.delete(location);
64+
throw new ExtensionManagementError((error as ExtensionSignatureVerificationError).code, ExtensionManagementErrorCode.Signature);
65+
}
66+
}
67+
68+
return { location, verified };
4769
}
4870

49-
async downloadSignatureArchive(extension: IGalleryExtension): Promise<URI> {
71+
private async downloadSignatureArchive(extension: IGalleryExtension): Promise<URI> {
5072
await this.cleanUpPromise;
5173

5274
const location = joinPath(this.extensionsDownloadDir, `${this.getName(extension)}${ExtensionsDownloader.SignatureArchiveExtension}`);
@@ -60,6 +82,12 @@ export class ExtensionsDownloader extends Disposable {
6082
return;
6183
}
6284

85+
// Download directly if locaiton is not file scheme
86+
if (location.scheme !== Schemas.file) {
87+
await downloadFn(location);
88+
return;
89+
}
90+
6391
// Download to temporary location first only if file does not exist
6492
const tempLocation = joinPath(this.extensionsDownloadDir, `.${generateUuid()}`);
6593
if (!await this.fileService.exists(tempLocation)) {

src/vs/platform/extensionManagement/node/extensionManagementService.ts

Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ import { IProductService } from 'vs/platform/product/common/productService';
4444
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
4545
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
4646
import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile';
47-
import { ExtensionSignatureVerificationError, IExtensionSignatureVerificationService } from 'vs/platform/extensionManagement/node/extensionSignatureVerificationService';
48-
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
4947

5048
interface InstallableExtension {
5149
zipPath: string;
@@ -80,9 +78,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
8078
@IFileService private readonly fileService: IFileService,
8179
@IProductService productService: IProductService,
8280
@IUriIdentityService uriIdentityService: IUriIdentityService,
83-
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService,
84-
@IConfigurationService private readonly configurationService: IConfigurationService,
85-
@IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService
81+
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
8682
) {
8783
super(galleryService, telemetryService, logService, productService, userDataProfilesService);
8884
const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle));
@@ -181,6 +177,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
181177
return this.extensionsScanner.cleanUp(removeOutdated);
182178
}
183179

180+
async download(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
181+
const { location } = await this.extensionsDownloader.download(extension, operation);
182+
return location;
183+
}
184+
184185
private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise<void> }> {
185186
if (vsix.scheme === Schemas.file) {
186187
return { location: vsix, async cleanup() { } };
@@ -207,7 +208,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
207208
const key = ExtensionKey.create(extension).toString();
208209
installExtensionTask = this.installGalleryExtensionsTasks.get(key);
209210
if (!installExtensionTask) {
210-
this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.logService, this.configurationService, this.extensionSignatureVerificationService));
211+
this.installGalleryExtensionsTasks.set(key, installExtensionTask = new InstallGalleryExtensionTask(manifest, extension, options, this.extensionsDownloader, this.extensionsScanner, this.logService));
211212
installExtensionTask.waitUntilTaskIsFinished().then(() => this.installGalleryExtensionsTasks.delete(key));
212213
}
213214
}
@@ -600,8 +601,6 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask {
600601
private readonly extensionsDownloader: ExtensionsDownloader,
601602
extensionsScanner: ExtensionsScanner,
602603
logService: ILogService,
603-
private readonly configurationService: IConfigurationService,
604-
private readonly extensionVerificationService: IExtensionSignatureVerificationService
605604
) {
606605
super(gallery.identifier, gallery, options, extensionsScanner, logService);
607606
}
@@ -637,17 +636,22 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask {
637636
return { local, metadata };
638637
}
639638

640-
const zipPath = await this.downloadVSIX(this.gallery, this._operation);
639+
const { location, verified } = await this.extensionsDownloader.download(this.gallery, this._operation);
641640
try {
642-
this.wasVerified = await this.verifyExtensionSignature(zipPath);
643-
this.validateManifest(zipPath);
644-
const local = await this.installExtension({ zipPath, key: ExtensionKey.create(this.gallery), metadata }, token);
641+
this.wasVerified = !!verified;
642+
this.validateManifest(location.fsPath);
643+
const local = await this.installExtension({ zipPath: location.fsPath, key: ExtensionKey.create(this.gallery), metadata }, token);
645644
if (existingExtension && !this.options.profileLocation && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) {
646645
await this.extensionsScanner.setUninstalled(existingExtension);
647646
}
648647
return { local, metadata };
649648
} catch (error) {
650-
await this.deleteDownloadedFile(zipPath);
649+
try {
650+
await this.extensionsDownloader.delete(location);
651+
} catch (error) {
652+
/* Ignore */
653+
this.logService.warn(`Error while deleting the downloaded file`, location.toString(), getErrorMessage(error));
654+
}
651655
throw error;
652656
}
653657
}
@@ -660,43 +664,6 @@ export class InstallGalleryExtensionTask extends InstallExtensionTask {
660664
}
661665
}
662666

663-
private async verifyExtensionSignature(zipPath: string): Promise<boolean> {
664-
if (!this.gallery.isSigned) {
665-
return false;
666-
}
667-
if (!this.configurationService.getValue('extensions.verifySignature')) {
668-
return false;
669-
}
670-
const signatureArchivePath = (await this.extensionsDownloader.downloadSignatureArchive(this.gallery)).fsPath;
671-
try {
672-
return await this.extensionVerificationService.verify(zipPath, signatureArchivePath);
673-
} catch (error) {
674-
await this.deleteDownloadedFile(signatureArchivePath);
675-
throw new ExtensionManagementError((error as ExtensionSignatureVerificationError).code, ExtensionManagementErrorCode.Signature);
676-
}
677-
}
678-
679-
private async deleteDownloadedFile(filePath: string): Promise<void> {
680-
try {
681-
await this.extensionsDownloader.delete(URI.file(filePath));
682-
} catch (error) {
683-
/* Ignore */
684-
this.logService.warn(`Error while deleting the downloaded file`, filePath.toString(), getErrorMessage(error));
685-
}
686-
}
687-
688-
private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise<string> {
689-
let zipPath: string | undefined;
690-
try {
691-
this.logService.trace('Started downloading extension:', extension.identifier.id);
692-
zipPath = (await this.extensionsDownloader.downloadVSIX(extension, operation)).fsPath;
693-
this.logService.info('Downloaded extension:', extension.identifier.id, zipPath);
694-
} catch (error) {
695-
throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Download);
696-
}
697-
return zipPath;
698-
}
699-
700667
}
701668

702669
class InstallVSIXTask extends InstallExtensionTask {

0 commit comments

Comments
 (0)