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: re-add support for custom registries with auth #397

Merged
merged 8 commits into from Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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`)}`,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without testing it but I think that should be Basic instead of Bearer when using username and password. 🤔

@aduh95

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thankfully it's already fixed on main: #454

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry. Okay the fix seems not the be released yet, that might explain why it's not working. Sorry for bothering you.

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