Skip to content

Commit

Permalink
fix: re-add support for custom registries with auth (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 committed Mar 27, 2024
1 parent 082fabf commit d267753
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 8 deletions.
6 changes: 6 additions & 0 deletions sources/corepackUtils.ts
Expand Up @@ -174,6 +174,12 @@ export async function installVersion(installTarget: string, locator: Locator, {s
}
} else {
url = decodeURIComponent(version);
if (process.env.COREPACK_NPM_REGISTRY && url.startsWith(npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL)) {
url = url.replace(
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
() => process.env.COREPACK_NPM_REGISTRY!,
);
}
}

// Creating a temporary folder inside the install folder means that we
Expand Down
39 changes: 34 additions & 5 deletions sources/httpUtils.ts
@@ -1,20 +1,49 @@
import assert from 'assert';
import {UsageError} from 'clipanion';
import {once} from 'events';
import {stderr, stdin} from 'process';
import {Readable} from 'stream';
import assert from 'assert';
import {UsageError} from 'clipanion';
import {once} from 'events';
import {stderr, stdin} from 'process';
import {Readable} from 'stream';

import {DEFAULT_NPM_REGISTRY_URL} from './npmRegistryUtils';

async function fetch(input: string | URL, init?: RequestInit) {
if (process.env.COREPACK_ENABLE_NETWORK === `0`)
throw new UsageError(`Network access disabled by the environment; can't reach ${input}`);

const agent = await getProxyAgent(input);

if (typeof input === `string`)
input = new URL(input);

let headers = init?.headers;
const {username, password} = input;
if (username || password) {
headers = {
...headers,
authorization: `Bearer ${Buffer.from(`${username}:${password}`).toString(`base64`)}`,
};
input.username = input.password = ``;
} else if (input.origin === process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL) {
if (process.env.COREPACK_NPM_TOKEN) {
headers = {
...headers,
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
};
} else if (`COREPACK_NPM_PASSWORD` in process.env) {
headers = {
...headers,
authorization: `Bearer ${Buffer.from(`${process.env.COREPACK_NPM_USER}:${process.env.COREPACK_NPM_PASSWORD}`).toString(`base64`)}`,
};
}
}


let response;
try {
response = await globalThis.fetch(input, {
...init,
dispatcher: agent,
headers,
});
} catch (error) {
throw new Error(
Expand Down
155 changes: 155 additions & 0 deletions tests/_registryServer.mjs
@@ -0,0 +1,155 @@
import {createHash} from 'node:crypto';
import {once} from 'node:events';
import {createServer} from 'node:http';
import {gzipSync} from 'node:zlib';

function createSimpleTarArchive(fileName, fileContent, mode = 0o644) {
const contentBuffer = Buffer.from(fileContent);

const header = Buffer.alloc(512); // TAR headers are 512 bytes
header.write(fileName);
header.write(`100${mode.toString(8)} `, 100, 7, `utf-8`); // File mode (octal) followed by a space
header.write(`0001750 `, 108, 8, `utf-8`); // Owner's numeric user ID (octal) followed by a space
header.write(`0001750 `, 116, 8, `utf-8`); // Group's numeric user ID (octal) followed by a space
header.write(`${contentBuffer.length.toString(8)} `, 124, 12, `utf-8`); // File size in bytes (octal) followed by a space
header.write(`${Math.floor(Date.now() / 1000).toString(8)} `, 136, 12, `utf-8`); // Last modification time in numeric Unix time format (octal) followed by a space
header.fill(` `, 148, 156); // Fill checksum area with spaces for calculation
header.write(`ustar `, 257, 8, `utf-8`); // UStar indicator

// Calculate and write the checksum. Note: This is a simplified calculation not recommended for production
const checksum = header.reduce((sum, value) => sum + value, 0);
header.write(`${checksum.toString(8)}\0 `, 148, 8, `utf-8`); // Write checksum in octal followed by null and space


return Buffer.concat([
header,
contentBuffer,
Buffer.alloc(512 - (contentBuffer.length % 512)),
]);
}

const mockPackageTarGz = gzipSync(Buffer.concat([
createSimpleTarArchive(`package/bin/customPkgManager.js`, `#!/usr/bin/env node\nconsole.log("customPkgManager: Hello from custom registry");\n`, 0o755),
createSimpleTarArchive(`package/bin/pnpm.js`, `#!/usr/bin/env node\nconsole.log("pnpm: Hello from custom registry");\n`, 0o755),
createSimpleTarArchive(`package/bin/yarn.js`, `#!/usr/bin/env node\nconsole.log("yarn: Hello from custom registry");\n`, 0o755),
createSimpleTarArchive(`package/package.json`, JSON.stringify({bin: {yarn: `bin/yarn.js`, pnpm: `bin/pnpm.js`, customPkgManager: `bin/customPkgManager.js`}})),
Buffer.alloc(1024),
]));
const shasum = createHash(`sha1`).update(mockPackageTarGz).digest(`hex`);


const server = createServer((req, res) => {
const auth = req.headers.authorization;
if (!auth?.startsWith(`Bearer `) || Buffer.from(auth.slice(`Bearer `.length), `base64`).toString() !== `user:pass`) {
res.statusCode = 401;
res.end(`Unauthorized`);
return;
}
switch (req.url) {
case `/yarn`: {
res.end(JSON.stringify({"dist-tags": {
latest: `1.9998.9999`,
}, versions: {'1.9998.9999': {
dist: {
shasum,
size: mockPackageTarGz.length,
noattachment: false,
tarball: `${process.env.COREPACK_NPM_REGISTRY}/yarn.tgz`,
},
}}}));
break;
}

case `/pnpm`: {
res.end(JSON.stringify({"dist-tags": {
latest: `1.9998.9999`,
}, versions: {'1.9998.9999': {
dist: {
shasum,
size: mockPackageTarGz.length,
noattachment: false,
tarball: `${process.env.COREPACK_NPM_REGISTRY}/pnpm/-/pnpm-1.9998.9999.tgz`,
},
}}}));
break;
}

case `/@yarnpkg/cli-dist`: {
res.end(JSON.stringify({"dist-tags": {
latest: `5.9999.9999`,
}, versions: {'5.9999.9999': {
bin: {
yarn: `./bin/yarn.js`,
yarnpkg: `./bin/yarn.js`,
},
dist: {
shasum,
size: mockPackageTarGz.length,
noattachment: false,
tarball: `${process.env.COREPACK_NPM_REGISTRY}/yarn.tgz`,
},
}}}));
break;
}

case `/customPkgManager`: {
res.end(JSON.stringify({"dist-tags": {
latest: `1.0.0`,
}, versions: {'1.0.0': {
bin: {
customPkgManager: `./bin/customPkgManager.js`,
},
dist: {
shasum,
size: mockPackageTarGz.length,
noattachment: false,
tarball: `${process.env.COREPACK_NPM_REGISTRY}/customPkgManager/-/customPkgManager-1.0.0.tgz`,
},
}}}));
break;
}

case `/pnpm/-/pnpm-1.9998.9999.tgz`:
case `/yarn.tgz`:
case `/customPkgManager/-/customPkgManager-1.0.0.tgz`:
res.end(mockPackageTarGz);
break;

default:
throw new Error(`unsupported request`, {cause: req.url});
}
}).listen(0, `localhost`);

await once(server, `listening`);

const {address, port} = server.address();
switch (process.env.AUTH_TYPE) {
case `COREPACK_NPM_REGISTRY`:
process.env.COREPACK_NPM_REGISTRY = `http://user:pass@${address.includes(`:`) ? `[${address}]` : address}:${port}`;
break;

case `COREPACK_NPM_TOKEN`:
process.env.COREPACK_NPM_REGISTRY = `http://${address.includes(`:`) ? `[${address}]` : address}:${port}`;
process.env.COREPACK_NPM_TOKEN = Buffer.from(`user:pass`).toString(`base64`);
break;

case `COREPACK_NPM_PASSWORD`:
process.env.COREPACK_NPM_REGISTRY = `http://${address.includes(`:`) ? `[${address}]` : address}:${port}`;
process.env.COREPACK_NPM_USER = `user`;
process.env.COREPACK_NPM_PASSWORD = `pass`;
break;

default: throw new Error(`Invalid AUTH_TYPE in env`, {cause: process.env.AUTH_TYPE});
}

if (process.env.NOCK_ENV === `replay`) {
const originalFetch = globalThis.fetch;
globalThis.fetch = function fetch(i) {
if (!`${i}`.startsWith(`http://${address.includes(`:`) ? `[${address}]` : address}:${port}`))
throw new Error;

return Reflect.apply(originalFetch, this, arguments);
};
}

server.unref();
6 changes: 4 additions & 2 deletions tests/_runCli.ts
@@ -1,12 +1,14 @@
import {PortablePath, npath} from '@yarnpkg/fslib';
import {spawn} from 'child_process';
import * as path from 'path';
import {pathToFileURL} from 'url';

export async function runCli(cwd: PortablePath, argv: Array<string>): Promise<{exitCode: number | null, stdout: string, stderr: string}> {
export async function runCli(cwd: PortablePath, argv: Array<string>, withCustomRegistry?: boolean): Promise<{exitCode: number | null, stdout: string, stderr: string}> {
const out: Array<Buffer> = [];
const err: Array<Buffer> = [];

return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [`--no-warnings`, `-r`, require.resolve(`./recordRequests.js`), require.resolve(`../dist/corepack.js`), ...argv], {
const child = spawn(process.execPath, [`--no-warnings`, ...(withCustomRegistry ? [`--import`, pathToFileURL(path.join(__dirname, `_registryServer.mjs`)) as any as string] : [`-r`, require.resolve(`./recordRequests.js`)]), require.resolve(`../dist/corepack.js`), ...argv], {
cwd: npath.fromPortablePath(cwd),
env: process.env,
stdio: `pipe`,
Expand Down
54 changes: 53 additions & 1 deletion tests/main.test.ts
@@ -1,4 +1,4 @@
import {beforeEach, it, expect} from '@jest/globals';
import {beforeEach, describe, expect, it} from '@jest/globals';
import {Filename, ppath, xfs, npath, PortablePath} from '@yarnpkg/fslib';
import process from 'node:process';

Expand Down Expand Up @@ -804,3 +804,55 @@ it(`should download yarn berry from custom registry`, async () => {
});
});
});

for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK_NPM_PASSWORD`]) {
describe(`custom registry with auth ${authType}`, () => {
beforeEach(() => {
process.env.AUTH_TYPE = authType; // See `_registryServer.mjs`
});

it(`should download yarn classic`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``,
});
});
});

it(`should download yarn berry`, async () => {
await xfs.mktempPromise(async cwd => {
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
packageManager: `yarn@3.0.0`,
});

await expect(runCli(cwd, [`yarn@5.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `yarn: Hello from custom registry\n`,
stderr: ``,
});
});
});

it(`should download pnpm`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `pnpm: Hello from custom registry\n`,
stderr: ``,
});
});
});

it(`should download custom package manager`, async () => {
await xfs.mktempPromise(async cwd => {
await expect(runCli(cwd, [`customPkgManager@https://registry.npmjs.org/customPkgManager/-/customPkgManager-1.0.0.tgz`, `--version`], true)).resolves.toMatchObject({
exitCode: 0,
stdout: `customPkgManager: Hello from custom registry\n`,
stderr: ``,
});
});
});
});
}

0 comments on commit d267753

Please sign in to comment.