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
Show file tree
Hide file tree
Changes from 15 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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ parameters:
default: ''
type: string
repo_tag:
description: 'The tag of the module repo to checkout, '''' defaults to branch/PR'
description: "The tag of the module repo to checkout, '' defaults to branch/PR"
default: ''
type: string
npm_module_name:
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
"@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": {
"@oclif/dev-cli": "^1",
"@oclif/plugin-command-snapshot": "^2.0.0",
"@salesforce/cli-plugins-testkit": "^1.1.5",
"@salesforce/cli-plugins-testkit": "^1.3.0",
"@salesforce/dev-config": "^2.1.2",
"@salesforce/dev-scripts": "0.9.11",
"@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
227 changes: 86 additions & 141 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, NpmMeta } 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,7 @@ export interface Verifier {
verify(): Promise<NpmMeta>;
isAllowListed(): Promise<boolean>;
}

export class CodeVerifierInfo {
private signature: Readable;
private publicKey: Readable;
Expand Down Expand Up @@ -140,17 +136,6 @@ export const getNpmRegistry = (): URL => {
return new URL(process.env.SFDX_NPM_REGISTRY || DEFAULT_REGISTRY);
};

/**
* simple data structure representing the discovered meta information needed for signing,
*/
export class NpmMeta {
public tarballUrl: string;
public signatureUrl: string;
public publicKeyUrl: string;
public tarballLocalPath: string;
public verified: boolean;
}

/**
* class for verifying a digital signature pack of an npm
*/
Expand Down Expand Up @@ -312,6 +297,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 +306,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 +334,86 @@ 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();

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);
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}`);

// 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'
)
);
}
const npmShowModule = this.pluginNpmName.scope
? `@${this.pluginNpmName.scope}/${this.pluginNpmName.name}`
: this.pluginNpmName.name;

// 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'));
}
}
const npmModule = new NpmModule(npmShowModule);
const npmMetadata = npmModule.show(npmRegistry.href);
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'
);
}

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);
}
// Assume the tag is version tag.
let versionNumber = npmMetadata.versions.find((version) => version === this.pluginNpmName.tag);

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

// 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 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');
}
}

npmModule.npmMeta.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}`);
npmModule.npmMeta.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
npmModule.npmMeta.signatureUrl = npmMetadata.sfdx.signatureUrl;
}

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

return npmModule.npmMeta;
}
}

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