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

fix: use npm instead of http #154

Merged
merged 16 commits into from
Sep 7, 2021
Merged
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"@oclif/config": "^1.17.0",
"@salesforce/command": "^3.0.5",
"@salesforce/core": "^2.23.2",
"npm": "^7.21.0",
"npm-run-path": "^4.0.1",
"shelljs": "^0.8.4",
"tslib": "^2"
},
"devDependencies": {
Expand All @@ -19,6 +22,7 @@
"@salesforce/plugin-command-reference": "^1.3.0",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/ts-sinon": "1.3.21",
"@types/shelljs": "^0.8.9",
"@typescript-eslint/eslint-plugin": "^4.2.0",
"@typescript-eslint/parser": "^4.2.0",
"chai": "^4.2.0",
Expand Down
234 changes: 104 additions & 130 deletions src/lib/installationVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,15 @@ import { parse as parseUrl, URL, UrlWithStringQuery } from 'url';
import { promisify as utilPromisify } from 'util';
import * as crypto from 'crypto';
import { Logger, fs, SfdxError } from '@salesforce/core';
import { get } from '@salesforce/ts-types';
import * as request from 'request';
import { NpmModule } from '../lib/npmCommand';
import { NpmName } from './NpmName';

const CRYPTO_LEVEL = 'RSA-SHA256';
export const ALLOW_LIST_FILENAME = 'unsignedPluginAllowList.json';
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
export type IRequest = (url: string, cb?: request.RequestCallback) => Readable;
type Version = {
sfdx: NpmMeta;
dist: {
tarball: string;
};
};
export type IRequest = (url: string, cb?: request.RequestCallback) => void;

export interface ConfigContext {
configDir?: string;
cacheDir?: string;
Expand All @@ -39,6 +34,21 @@ export interface Verifier {
verify(): Promise<NpmMeta>;
isAllowListed(): Promise<boolean>;
}

export type NpmShowResults = {
versions: string[];
'dist-tags': {
[name: string]: string;
};
sfdx?: {
publicKeyUrl: string;
signatureUrl: string;
};
dist?: {
[name: string]: string;
};
};

export class CodeVerifierInfo {
private signature: Readable;
private publicKey: Readable;
Expand Down Expand Up @@ -149,6 +159,9 @@ export class NpmMeta {
public publicKeyUrl: string;
public tarballLocalPath: string;
public verified: boolean;
public moduleName: string;
public version: string;
public tarballFilename: string;
}

/**
Expand Down Expand Up @@ -312,6 +325,7 @@ export class InstallationVerification implements Verifier {
const npmMeta = await this.retrieveNpmMeta();
const urlObject: URL = new URL(npmMeta.tarballUrl);
const urlPathsAsArray = urlObject.pathname.split('/');
npmMeta.tarballFilename = npmMeta.moduleName.replace(/@/g, '');
logger.debug(`streamTagGz | urlPathsAsArray: ${urlPathsAsArray.join(',')}`);

const fileNameStr: string = urlPathsAsArray[urlPathsAsArray.length - 1];
Expand All @@ -320,27 +334,17 @@ export class InstallationVerification implements Verifier {
// Make sure the cache path exists.
try {
await fs.mkdirp(this.getCachePath());
new NpmModule(npmMeta.moduleName, npmMeta.version).pack(getNpmRegistry().href, { cwd: this.getCachePath() });
const tarBallFile = fs
.readdirSync(this.getCachePath(), { withFileTypes: true })
.find((entry) => entry.isFile() && entry.name.includes(npmMeta.version));
npmMeta.tarballLocalPath = path.join(this.getCachePath(), tarBallFile.name);
} catch (err) {
logger.debug(err);
throw new SfdxError(err, 'ShellExecError');
}

return new Promise<NpmMeta>((resolve, reject) => {
const cacheFilePath = path.join(this.getCachePath(), fileNameStr);
logger.debug(`streamTagGz | cacheFilePath: ${cacheFilePath}`);

const writeStream = this.fsImpl.createWriteStream(cacheFilePath, { encoding: 'binary' });
this.requestImpl(npmMeta.tarballUrl)
.on('end', () => {
logger.debug('streamTagGz | Finished writing tgz file');
npmMeta.tarballLocalPath = cacheFilePath;
return resolve(npmMeta);
})
.on('error', (err) => {
logger.debug(err);
return reject(err);
})
.pipe(writeStream);
});
return npmMeta;
}

// this is generally $HOME/.config/sfdx
Expand All @@ -358,117 +362,87 @@ export class InstallationVerification implements Verifier {
*/
private async retrieveNpmMeta(): Promise<NpmMeta> {
const logger = await this.getLogger();
return new Promise<NpmMeta>((resolve, reject) => {
const npmRegistry = getNpmRegistry();

logger.debug(`retrieveNpmMeta | npmRegistry: ${npmRegistry.href}`);
logger.debug(`retrieveNpmMeta | this.pluginNpmName.name: ${this.pluginNpmName.name}`);
logger.debug(`retrieveNpmMeta | this.pluginNpmName.scope: ${this.pluginNpmName.scope}`);
logger.debug(`retrieveNpmMeta | this.pluginNpmName.tag: ${this.pluginNpmName.tag}`);

if (this.pluginNpmName.scope) {
npmRegistry.pathname = path.join(
npmRegistry.pathname,
`@${this.pluginNpmName.scope}%2f${this.pluginNpmName.name}`
);
} else {
npmRegistry.pathname = path.join(npmRegistry.pathname, this.pluginNpmName.name);
}
logger.debug(`retrieveNpmMeta | npmRegistry.pathname: ${npmRegistry.pathname}`);
const npmRegistry = getNpmRegistry();

logger.debug(`retrieveNpmMeta | npmRegistry: ${npmRegistry.href}`);
logger.debug(`retrieveNpmMeta | this.pluginNpmName.name: ${this.pluginNpmName.name}`);
logger.debug(`retrieveNpmMeta | this.pluginNpmName.scope: ${this.pluginNpmName.scope}`);
logger.debug(`retrieveNpmMeta | this.pluginNpmName.tag: ${this.pluginNpmName.tag}`);

const npmShowModule = this.pluginNpmName.scope
? `@${this.pluginNpmName.scope}/${this.pluginNpmName.name}`
: this.pluginNpmName.name;

const npmMetadata = new NpmModule(npmShowModule).show(npmRegistry.href);
const meta: NpmMeta = new NpmMeta();
maggiben marked this conversation as resolved.
Show resolved Hide resolved
meta.moduleName = npmShowModule;
logger.debug('retrieveNpmMeta | Found npm meta information.');
if (!npmMetadata.versions) {
throw new SfdxError(
`The npm metadata for plugin ${this.pluginNpmName.name} is missing the versions attribute.`,
'InvalidNpmMetadata'
);
}

this.requestImpl(npmRegistry.href, (err, response, body) => {
if (err) {
return reject(err);
}
if (response && response.statusCode === 200) {
logger.debug('retrieveNpmMeta | Found npm meta information. Parsing.');
const responseObj = JSON.parse(body);
// Assume the tag is version tag.
let versionNumber = npmMetadata.versions.find((version) => version === this.pluginNpmName.tag);

// Make sure the response has a version attribute
if (!responseObj.versions) {
return reject(
new SfdxError(
`The npm metadata for plugin ${this.pluginNpmName.name} is missing the versions attribute.`,
'InvalidNpmMetadata'
)
);
}
logger.debug(`retrieveNpmMeta | versionObject: ${JSON.stringify(versionNumber)}`);

// Assume the tag is version tag.
let versionObject: Version = responseObj.versions[this.pluginNpmName.tag];

logger.debug(`retrieveNpmMeta | versionObject: ${JSON.stringify(versionObject)}`);

// If the assumption was not correct the tag must be a non-versioned dist-tag or not specified.
if (!versionObject) {
// Assume dist-tag;
const distTags: string = get(responseObj, 'dist-tags') as string;
logger.debug(`retrieveNpmMeta | distTags: ${distTags}`);
if (distTags) {
const tagVersionStr: string = get(distTags, this.pluginNpmName.tag) as string;
logger.debug(`retrieveNpmMeta | tagVersionStr: ${tagVersionStr}`);

// if we got a dist tag hit look up the version object
if (tagVersionStr && tagVersionStr.length > 0 && tagVersionStr.includes('.')) {
versionObject = responseObj.versions[tagVersionStr];
logger.debug(`retrieveNpmMeta | versionObject: ${JSON.stringify(versionObject)}`);
} else {
return reject(
new SfdxError(
`The dist tag ${this.pluginNpmName.tag} was not found for plugin: ${this.pluginNpmName.name}`,
'NpmTagNotFound'
)
);
}
} else {
return reject(new SfdxError('The deployed NPM is missing dist-tags.', 'UnexpectedNpmFormat'));
}
}
// If the assumption was not correct the tag must be a non-versioned dist-tag or not specified.
if (!versionNumber) {
// Assume dist-tag;
const distTags = npmMetadata['dist-tags'];
logger.debug(`retrieveNpmMeta | distTags: ${JSON.stringify(distTags)}`);
if (distTags) {
const tagVersionStr: string = distTags[this.pluginNpmName.tag];
logger.debug(`retrieveNpmMeta | tagVersionStr: ${tagVersionStr}`);

if (!(versionObject && versionObject.sfdx)) {
return reject(new SfdxError('This plugin is not signed by Salesforce.com, Inc.', 'NotSigned'));
} else {
const meta: NpmMeta = new NpmMeta();
if (!validSalesforceHostname(versionObject.sfdx.publicKeyUrl)) {
throw new SfdxError(
`The host is not allowed to provide signing information. [${versionObject.sfdx.publicKeyUrl}]`,
'UnexpectedHost'
);
} else {
logger.debug(`retrieveNpmMeta | versionObject.sfdx.publicKeyUrl: ${versionObject.sfdx.publicKeyUrl}`);
meta.publicKeyUrl = versionObject.sfdx.publicKeyUrl;
}

if (!validSalesforceHostname(versionObject.sfdx.signatureUrl)) {
throw new SfdxError(
`The host is not allowed to provide signing information. [${versionObject.sfdx.signatureUrl}]`,
'UnexpectedHost'
);
} else {
logger.debug(`retrieveNpmMeta | versionObject.sfdx.signatureUrl: ${versionObject.sfdx.signatureUrl}`);
meta.signatureUrl = versionObject.sfdx.signatureUrl;
}

meta.tarballUrl = versionObject.dist.tarball;
logger.debug(`retrieveNpmMeta | meta.tarballUrl: ${meta.tarballUrl}`);

return resolve(meta);
}
// if we got a dist tag hit look up the version object
if (tagVersionStr && tagVersionStr.length > 0 && tagVersionStr.includes('.')) {
versionNumber = npmMetadata.versions.find((version) => version === tagVersionStr);
logger.debug(`retrieveNpmMeta | versionObject: ${versionNumber}`);
} else {
switch (response.statusCode) {
case 403:
throw new SfdxError(`Access to the plugin was denied. url: ${npmRegistry.href}`, 'PluginAccessDenied');
case 404:
throw new SfdxError(`The plugin requested was not found. url: ${npmRegistry.href}.`, 'PluginNotFound');
default:
throw new SfdxError(
`The url request returned ${response.statusCode as string} - ${npmRegistry.href}`,
'UrlRetrieve'
);
}
throw new SfdxError(
`The dist tag ${this.pluginNpmName.tag} was not found for plugin: ${this.pluginNpmName.name}`,
'NpmTagNotFound'
);
}
});
});
} else {
throw new SfdxError('The deployed NPM is missing dist-tags.', 'UnexpectedNpmFormat');
}
}

meta.version = versionNumber;

if (!npmMetadata.sfdx) {
throw new SfdxError('This plugin is not signed by Salesforce.com, Inc.', 'NotSigned');
} else {
if (!validSalesforceHostname(npmMetadata.sfdx.publicKeyUrl)) {
throw new SfdxError(
`The host is not allowed to provide signing information. [${npmMetadata.sfdx.publicKeyUrl}]`,
'UnexpectedHost'
);
} else {
logger.debug(`retrieveNpmMeta | versionObject.sfdx.publicKeyUrl: ${npmMetadata.sfdx.publicKeyUrl}`);
meta.publicKeyUrl = npmMetadata.sfdx.publicKeyUrl;
}

if (!validSalesforceHostname(npmMetadata.sfdx.signatureUrl)) {
throw new SfdxError(
`The host is not allowed to provide signing information. [${npmMetadata.sfdx.signatureUrl}]`,
'UnexpectedHost'
);
} else {
logger.debug(`retrieveNpmMeta | versionObject.sfdx.signatureUrl: ${npmMetadata.sfdx.signatureUrl}`);
maggiben marked this conversation as resolved.
Show resolved Hide resolved
meta.signatureUrl = npmMetadata.sfdx.signatureUrl;
}

meta.tarballUrl = npmMetadata.dist.tarball;
logger.debug(`retrieveNpmMeta | meta.tarballUrl: ${meta.tarballUrl}`);

return meta;
}
}

private async getLogger(): Promise<Logger> {
Expand Down
Loading