Skip to content

Commit

Permalink
feat: tarball url redirect (#1688)
Browse files Browse the repository at this point in the history
* feat: tarball url redirect

* fix: handle uplinks

* feat: allow function for config.tarball_url_redirect

* fix:  hasLocalTarball was calling localStream,abort when already aborted

* chore: simplify localStream null check in hasLocalTarball

As requested in PR feedback.

* chore: fix sonarcloud code smell on test

the variable `credentials` was already declared before the tarball url tests.

* fix: move tarball_url_redirect to experiments

Co-authored-by: Gord Lea <johlea@cisco.com>
Co-authored-by: Gord Lea <jgordonlea@gmail.com>
  • Loading branch information
3 people committed May 3, 2021
1 parent 255650b commit 78d04cf
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 3 deletions.
7 changes: 7 additions & 0 deletions conf/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ logs: { type: stdout, format: pretty, level: http }
# search: false
# # disable writing body size to logs, read more on ticket 1912
# bytesin_off: false
# # enable tarball URL redirect for hosting tarball with a different server, the tarball_url_redirect can be a template string
# tarball_url_redirect: 'https://mycdn.com/verdaccio/${packageName}/${filename}'
# # the tarball_url_redirect can be a function, takes packageName and filename and returns the url, when working with a js configuration file
# tarball_url_redirect(packageName, filename) {
# const signedUrl = // generate a signed url
# return signedUrl;
# }

# This affect the web and api (not developed yet)
#i18n:
Expand Down
7 changes: 7 additions & 0 deletions conf/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ logs: { type: stdout, format: pretty, level: http }
# token: false
# # support for the new v1 search endpoint, functional by incomplete read more on ticket 1732
# search: false
# # enable tarball URL redirect for hosting tarball with a different server, the tarball_url_redirect can be a template string
# tarball_url_redirect: 'https://mycdn.com/verdaccio/${packageName}/${filename}'
# # the tarball_url_redirect can be a function, takes packageName and filename and returns the url, when working with a js configuration file
# tarball_url_redirect(packageName, filename) {
# const signedUrl = // generate a signed url
# return signedUrl;
# }

# This affect the web and api (not developed yet)
#i18n:
Expand Down
37 changes: 34 additions & 3 deletions src/api/endpoint/api/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ const downloadStream = (
stream.pipe(res);
};

const redirectOrDownloadStream = (
packageName: string,
filename: string,
storage: any,
req: $RequestExtend,
res: $ResponseExtend,
config: Config
): void => {
const tarballUrlRedirect = _.get(config, 'experiments.tarball_url_redirect');
storage.hasLocalTarball(packageName, filename).then(hasLocalTarball => {
if (hasLocalTarball) {
const context = { packageName, filename };
const tarballUrl = typeof tarballUrlRedirect === 'function'
? tarballUrlRedirect(context)
: _.template(tarballUrlRedirect)(context);
res.redirect(tarballUrl);
} else {
downloadStream(packageName, filename, storage, req, res)
}
}).catch(err => {
res.locals.report_error(err);
});
}

export default function (
route: Router,
auth: IAuth,
Expand Down Expand Up @@ -87,16 +111,23 @@ export default function (
can('access'),
function (req: $RequestExtend, res: $ResponseExtend): void {
const { scopedPackage, filename } = req.params;

downloadStream(scopedPackage, filename, storage, req, res);
if (_.get(config, 'experiments.tarball_url_redirect') === undefined) {
downloadStream(scopedPackage, filename, storage, req, res);
} else {
redirectOrDownloadStream(scopedPackage, filename, storage, req, res, config);
}
}
);

route.get(
'/:package/-/:filename',
can('access'),
function (req: $RequestExtend, res: $ResponseExtend): void {
downloadStream(req.params.package, req.params.filename, storage, req, res);
if (_.get(config, 'experiments.tarball_url_redirect') === undefined) {
downloadStream(req.params.package, req.params.filename, storage, req, res);
} else {
redirectOrDownloadStream(req.params.package, req.params.filename, storage, req, res, config);
}
}
);
}
1 change: 1 addition & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const HTTP_STATUS = {
OK: 200,
CREATED: 201,
MULTIPLE_CHOICES: 300,
REDIRECT: 302,
NOT_MODIFIED: 304,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
Expand Down
28 changes: 28 additions & 0 deletions src/lib/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,34 @@ class Storage implements IStorageHandler {
return this.localStorage.addTarball(name, filename);
}

public hasLocalTarball(name: string, filename: string): Promise<boolean> {
const self = this;
return new Promise<boolean>((resolve, reject): void => {
let localStream: any = self.localStorage.getTarball(name, filename);
let isOpen = false;
localStream.on(
'error',
(err): any => {
if (isOpen || err.status !== HTTP_STATUS.NOT_FOUND) {
reject(err);
}
// local reported 404 or request was aborted already
if (localStream) {
localStream.abort();
localStream = null;
}
resolve(false);
}
);
localStream.on('open', function(): void {
isOpen = true;
localStream.abort();
localStream = null;
resolve(true);
});
});
}

/**
Get a tarball from a storage for {name} package
Function is synchronous and returns a ReadableStream
Expand Down
118 changes: 118 additions & 0 deletions test/unit/modules/api/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,124 @@ describe('endpoint unit test', () => {
});
});

describe('should test tarball url redirect', () => {
const pkgName = 'testTarballPackage';
const scopedPkgName = '@tarball_tester/testTarballPackage';
const tarballUrlRedirectCredentials = { name: 'tarball_tester', password: 'secretPass' };
const store = path.join(__dirname, '../../partials/store/test-storage-api-spec');
const mockServerPort = 55549;
const baseTestConfig = configDefault({
auth: {
htpasswd: {
file: './test-storage-api-spec/.htpasswd'
}
},
filters: {
'../../modules/api/partials/plugin/filter': {
pkg: 'npm_test',
version: '2.0.0'
}
},
storage: store,
self_path: store,
uplinks: {
npmjs: {
url: `http://${DOMAIN_SERVERS}:${mockServerPort}`
}
},
logs: [
{ type: 'stdout', format: 'pretty', level: 'warn' }
],
}, 'api.spec.yaml');
let token;
beforeAll(async (done) => {
token = await getNewToken(request(app), tarballUrlRedirectCredentials);
await putPackage(request(app), `/${pkgName}`, generatePackageMetadata(pkgName), token);
await putPackage(request(app), `/${scopedPkgName}`, generatePackageMetadata(scopedPkgName), token);
done();
});

describe('for a string value of tarball_url_redirect', () => {
let app2;
beforeAll(async (done) => {
app2 = await endPointAPI({
...baseTestConfig,
experiments: {
tarball_url_redirect: 'https://myapp.sfo1.mycdn.com/verdaccio/${packageName}/${filename}'
}
});
done();
});

test('should redirect for package tarball', (done) => {
request(app2)
.get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz')
.expect(HTTP_STATUS.REDIRECT)
.end(function (err, res) {
if (err) {
return done(err);
}
expect(res.headers.location).toEqual('https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz');
done();
});
});

test('should redirect for scoped package tarball', (done) => {
request(app2)
.get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz')
.expect(HTTP_STATUS.REDIRECT)
.end(function (err, res) {
if (err) {
return done(err);
}
expect(res.headers.location).toEqual('https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz');
done();
});
});
});

describe('for a function value of tarball_url_redirect', () => {
let app2;
beforeAll(async (done) => {
app2 = await endPointAPI({
...baseTestConfig,
experiments: {
tarball_url_redirect(context) {
return `https://myapp.sfo1.mycdn.com/verdaccio/${context.packageName}/${context.filename}`
}
}
});
done();
});

test('should redirect for package tarball', (done) => {
request(app2)
.get('/testTarballPackage/-/testTarballPackage-1.0.0.tgz')
.expect(HTTP_STATUS.REDIRECT)
.end(function (err, res) {
if (err) {
return done(err);
}
expect(res.headers.location).toEqual('https://myapp.sfo1.mycdn.com/verdaccio/testTarballPackage/testTarballPackage-1.0.0.tgz');
done();
});
});

test('should redirect for scoped package tarball', (done) => {
request(app2)
.get('/@tarball_tester/testTarballPackage/-/testTarballPackage-1.0.0.tgz')
.expect(HTTP_STATUS.REDIRECT)
.end(function (err, res) {
if (err) {
return done(err);
}
expect(res.headers.location).toEqual('https://myapp.sfo1.mycdn.com/verdaccio/@tarball_tester/testTarballPackage/testTarballPackage-1.0.0.tgz');
done();
});
});
});
});

describe('should test (un)deprecate api', () => {
const pkgName = '@scope/deprecate';
const credentials = { name: 'jota_deprecate', password: 'secretPass' };
Expand Down

0 comments on commit 78d04cf

Please sign in to comment.