diff --git a/.gitignore b/.gitignore index 1a65dedf8..171f2f959 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ node_modules ### test test-storage* .verdaccio_test_env +packages/server/fastify/debug/storage # docker examples docker-examples/v5/reverse_proxy/nginx/relative_path/storage/* diff --git a/packages/core/tarball/src/index.ts b/packages/core/tarball/src/index.ts index 37fbb1684..dc8a66bca 100644 --- a/packages/core/tarball/src/index.ts +++ b/packages/core/tarball/src/index.ts @@ -7,3 +7,4 @@ export { export { getLocalRegistryTarballUri } from './getLocalRegistryTarballUri'; export { RequestOptions }; +export { getVersionFromTarball } from './utils'; diff --git a/packages/core/tarball/src/utils.ts b/packages/core/tarball/src/utils.ts new file mode 100644 index 000000000..cb79d6e9e --- /dev/null +++ b/packages/core/tarball/src/utils.ts @@ -0,0 +1,5 @@ +export function getVersionFromTarball(name: string): string | void { + const groups = name.match(/.+-(\d.+)\.tgz/); + + return groups !== null ? groups[1] : undefined; +} diff --git a/packages/server/fastify/debug/index.ts b/packages/server/fastify/debug/index.ts index 09e1c55c9..64e3127c5 100644 --- a/packages/server/fastify/debug/index.ts +++ b/packages/server/fastify/debug/index.ts @@ -17,11 +17,12 @@ const debug = buildDebug('verdaccio:fastify:debug'); const configFile = path.join(__dirname, './fastify-conf.yaml'); debug('configFile %s', configFile); const configParsed = parseConfigFile(configFile); + // @ts-ignore setup(configParsed.log); logger.info(`config location ${configFile}`); debug('configParsed %s', configParsed); process.title = 'fastify-verdaccio'; - const ser = await server({ logger, config: configParsed }); + const ser = await server(configParsed); await ser.listen(4873); logger.info('fastify running on port 4873'); } catch (err: any) { diff --git a/packages/server/fastify/src/endpoints/dist-tags.ts b/packages/server/fastify/src/endpoints/dist-tags.ts index 4512a390b..24b4919e3 100644 --- a/packages/server/fastify/src/endpoints/dist-tags.ts +++ b/packages/server/fastify/src/endpoints/dist-tags.ts @@ -3,6 +3,9 @@ import { FastifyInstance } from 'fastify'; import { MergeTags } from '@verdaccio/types'; +import allow from '../plugins/allow'; +import pkgMetadata from '../plugins/pkgMetadata'; + const debug = buildDebug('verdaccio:fastify:dist-tags'); interface ParamsInterface { @@ -10,6 +13,9 @@ interface ParamsInterface { } async function distTagsRoute(fastify: FastifyInstance) { + fastify.register(pkgMetadata); + fastify.register(allow, { type: 'access' }); + fastify.get<{ Params: ParamsInterface }>( '/-/package/:packageName/dist-tags', async (request, reply) => { diff --git a/packages/server/fastify/src/endpoints/manifest.ts b/packages/server/fastify/src/endpoints/manifest.ts index ec28d524b..d9dc57584 100644 --- a/packages/server/fastify/src/endpoints/manifest.ts +++ b/packages/server/fastify/src/endpoints/manifest.ts @@ -3,10 +3,13 @@ import { FastifyInstance } from 'fastify'; import { stringUtils } from '@verdaccio/core'; import { Storage } from '@verdaccio/store'; -import { Package, Version } from '@verdaccio/types'; +import { Manifest, Version } from '@verdaccio/types'; + +import allow from '../plugins/allow'; +import pkgMetadata from '../plugins/pkgMetadata'; const debug = buildDebug('verdaccio:fastify:api:sidebar'); -export type $SidebarPackage = Package & { latest: Version }; +export type $SidebarPackage = Manifest & { latest: Version }; interface ParamsInterface { name: string; @@ -14,6 +17,9 @@ interface ParamsInterface { } async function manifestRoute(fastify: FastifyInstance) { + fastify.register(pkgMetadata); + fastify.register(allow, { type: 'access' }); + fastify.get<{ Params: ParamsInterface }>('/:name', async (request) => { const { name } = request.params; const storage = fastify.storage; @@ -30,6 +36,8 @@ async function manifestRoute(fastify: FastifyInstance) { protocol: request.protocol, headers: request.headers as any, host: request.hostname, + // @ts-ignore + username: request?.userRemote?.name, }, abbreviated, }); @@ -41,7 +49,7 @@ async function manifestRoute(fastify: FastifyInstance) { } fastify.get<{ Params: ParamsInterface; Querystring: QueryInterface }>( - '/:packageName/:version', + '/:name/:version', async (request) => { const { name, version } = request.params; const storage = fastify.storage; diff --git a/packages/server/fastify/src/endpoints/ping.ts b/packages/server/fastify/src/endpoints/ping.ts index 311256044..371037969 100644 --- a/packages/server/fastify/src/endpoints/ping.ts +++ b/packages/server/fastify/src/endpoints/ping.ts @@ -1,12 +1,9 @@ -/* eslint-disable no-console */ - -/* eslint-disable no-invalid-this */ import { FastifyInstance } from 'fastify'; import { logger } from '@verdaccio/logger'; async function pingRoute(fastify: FastifyInstance) { - fastify.get('/-/ping', async () => { + fastify.get('/-/ping', () => { logger.http('ping'); return {}; }); diff --git a/packages/server/fastify/src/endpoints/search.ts b/packages/server/fastify/src/endpoints/search.ts index 52cc8ea03..9231c3579 100644 --- a/packages/server/fastify/src/endpoints/search.ts +++ b/packages/server/fastify/src/endpoints/search.ts @@ -18,7 +18,8 @@ async function searchRoute(fastify: FastifyInstance) { // TODO: add validations for query, some parameters are mandatory // TODO: review which query fields are mandatory const abort = new AbortController(); - request.socket.on('aborted', () => { + // https://nodejs.org/dist/latest-v18.x/docs/api/http.html#event-close + request.socket.on('close', () => { abort.abort(); }); const { url, query } = request.query; diff --git a/packages/server/fastify/src/endpoints/tarball.ts b/packages/server/fastify/src/endpoints/tarball.ts index 1cfafd932..e6b77edca 100644 --- a/packages/server/fastify/src/endpoints/tarball.ts +++ b/packages/server/fastify/src/endpoints/tarball.ts @@ -1,9 +1,11 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import buildDebug from 'debug'; import { FastifyInstance } from 'fastify'; import { HEADERS, HEADER_TYPE } from '@verdaccio/core'; +import allow from '../plugins/allow'; +import pkgMetadata from '../plugins/pkgMetadata'; + const debug = buildDebug('verdaccio:fastify:tarball'); interface ParamsInterface { @@ -12,6 +14,9 @@ interface ParamsInterface { } async function tarballRoute(fastify: FastifyInstance) { + fastify.register(pkgMetadata); + fastify.register(allow, { type: 'access' }); + fastify.get<{ Params: ParamsInterface }>('/:package/-/:filename', async (request, reply) => { const { package: pkg, filename } = request.params; debug('stream tarball for %s@%s', pkg, filename); @@ -25,10 +30,10 @@ async function tarballRoute(fastify: FastifyInstance) { reply.header(HEADER_TYPE.CONTENT_LENGTH, size); }); - // request.socket.on('abort', () => { - // debug('request aborted for %o', request.url); - // abort.abort(); - // }); + // https://nodejs.org/dist/latest-v18.x/docs/api/http.html#event-close + request.socket.on('close', () => { + abort.abort(); + }); return stream; }); @@ -55,10 +60,10 @@ async function tarballRoute(fastify: FastifyInstance) { reply.header(HEADER_TYPE.CONTENT_LENGTH, size); }); - // request.socket.on('abort', () => { - // debug('request aborted for %o', request.url); - // abort.abort(); - // }); + // https://nodejs.org/dist/latest-v18.x/docs/api/http.html#event-close + request.socket.on('close', () => { + abort.abort(); + }); reply.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM); return stream; diff --git a/packages/server/fastify/src/endpoints/user.ts b/packages/server/fastify/src/endpoints/user.ts index cf58e6cef..ba053f4cc 100644 --- a/packages/server/fastify/src/endpoints/user.ts +++ b/packages/server/fastify/src/endpoints/user.ts @@ -1,6 +1,3 @@ -/* eslint-disable no-console */ - -/* eslint-disable no-invalid-this */ import buildDebug from 'debug'; import { FastifyInstance } from 'fastify'; import _ from 'lodash'; @@ -39,6 +36,7 @@ async function userRoute(fastify: FastifyInstance) { const { token } = request.params; const userRemote: RemoteUser = request.userRemote; await fastify.auth.invalidateToken(token); + // eslint-disable-next-line no-console console.log('userRoute', userRemote); reply.code(fastify.statusCode.OK); return { ok: fastify.apiMessage.LOGGED_OUT }; diff --git a/packages/server/fastify/src/plugins/allow.ts b/packages/server/fastify/src/plugins/allow.ts new file mode 100644 index 000000000..40d2a243c --- /dev/null +++ b/packages/server/fastify/src/plugins/allow.ts @@ -0,0 +1,85 @@ +import { FastifyInstance } from 'fastify'; +import fp from 'fastify-plugin'; + +import { Auth } from '@verdaccio/auth'; +import { pluginUtils } from '@verdaccio/core'; +import { RemoteUser } from '@verdaccio/types'; + +/** + * TODO: use @verdaccio/tarball + * return package version from tarball name + * @param {String} name + * @deprecated use @verdaccio/tarball + * @returns {String} + */ +export function getVersionFromTarball(name: string): string | void { + const groups = name.match(/.+-(\d.+)\.tgz/); + + return groups !== null ? groups[1] : undefined; +} + +export default fp( + async (fastify: FastifyInstance, opts: { type: string }) => { + // ensure user remote is populated on every request + fastify.addHook('preValidation', async function (request) { + const remote: RemoteUser = request.userRemote; + + switch (opts.type) { + case 'access': + return new Promise((resolve, reject) => { + return fastify.auth.allow_access(request.pkgMetadata, remote, (err, allowed) => { + if (err) { + return reject(err); + } + if (allowed === true) { + return resolve(); + } + return reject(fastify.errorUtils.getForbidden()); + }); + }); + case 'publish': + return new Promise((resolve, reject) => { + return fastify.auth.allow_publish(request.pkgMetadata, remote, (err, allowed) => { + if (err) { + return reject(err); + } + if (allowed === true) { + return resolve(); + } + return reject(fastify.errorUtils.getForbidden()); + }); + }); + case 'unpublish': + return new Promise((resolve, reject) => { + return fastify.auth.allow_unpublish(request.pkgMetadata, remote, (err, allowed) => { + if (err) { + return reject(err); + } + if (allowed === true) { + return resolve(); + } + return reject(fastify.errorUtils.getForbidden()); + }); + }); + default: + } + }); + }, + { + fastify: '>=4.x', + } +); + +declare module 'fastify' { + // @ts-ignore + interface FastifyInstance { + auth: Auth; + } + + // @ts-ignore + interface FastifyRequest { + pkgMetadata: pluginUtils.AuthPluginPackage; + // TODO: scope not caugh yet. + pkgScope?: string; + } +} diff --git a/packages/server/fastify/src/plugins/auth.ts b/packages/server/fastify/src/plugins/auth.ts index 8ea758a40..a95d20ef0 100644 --- a/packages/server/fastify/src/plugins/auth.ts +++ b/packages/server/fastify/src/plugins/auth.ts @@ -17,6 +17,7 @@ export default fp( ); declare module 'fastify' { + // @ts-ignore interface FastifyInstance { auth: Auth; } diff --git a/packages/server/fastify/src/plugins/config.ts b/packages/server/fastify/src/plugins/config.ts index b0eccd8b5..59c780eff 100644 --- a/packages/server/fastify/src/plugins/config.ts +++ b/packages/server/fastify/src/plugins/config.ts @@ -16,6 +16,7 @@ export default fp( ); declare module 'fastify' { + // @ts-ignore interface FastifyInstance { configInstance: IConfig; } diff --git a/packages/server/fastify/src/plugins/coreUtils.ts b/packages/server/fastify/src/plugins/coreUtils.ts index adafdad3b..4aad0ca6f 100644 --- a/packages/server/fastify/src/plugins/coreUtils.ts +++ b/packages/server/fastify/src/plugins/coreUtils.ts @@ -35,6 +35,7 @@ export default fp( ); declare module 'fastify' { + // @ts-ignore interface FastifyInstance { apiError: typeof API_ERROR; apiMessage: typeof API_MESSAGE; diff --git a/packages/server/fastify/src/plugins/pkgMetadata.ts b/packages/server/fastify/src/plugins/pkgMetadata.ts new file mode 100644 index 000000000..a56518d77 --- /dev/null +++ b/packages/server/fastify/src/plugins/pkgMetadata.ts @@ -0,0 +1,63 @@ +import { FastifyInstance } from 'fastify'; +import fp from 'fastify-plugin'; + +import { Auth } from '@verdaccio/auth'; +import { pluginUtils } from '@verdaccio/core'; +import { Manifest } from '@verdaccio/types'; + +/** + * TODO: use @verdaccio/tarball + * return package version from tarball name + * @param {String} name + * @deprecated use @verdaccio/tarball + * @returns {String} + */ +export function getVersionFromTarball(name: string): string | void { + const groups = name.match(/.+-(\d.+)\.tgz/); + + return groups !== null ? groups[1] : undefined; +} + +export default fp( + async (fastify: FastifyInstance) => { + fastify.addHook<{ + Params: { scope: string; name: string; filename?: string; version?: string; tag?: string }; + Body: Manifest; + }>('onRequest', function (request, _reply, done) { + // TODO: scope is not implemented yet + const { scope, name, tag, filename } = request.params; + const packageName = typeof scope === 'string' ? `@${scope}/${name}` : name; + // version could be a param initially + let packageVersion: string | undefined = request.params.version; + // when request is tarball request version comes with the tarball + // eg: http://localhost:4873/@angular/cli/-/cli-8.3.5.tgz + if (filename && typeof packageVersion !== 'string') { + packageVersion = getVersionFromTarball(filename) || undefined; + } + const pkgMetadata: pluginUtils.AuthPluginPackage = { + packageName, + packageVersion, + tag, + }; + + request.pkgMetadata = pkgMetadata; + + done(); + }); + }, + { + fastify: '>=4.x', + } +); + +declare module 'fastify' { + // @ts-ignore + interface FastifyInstance { + auth: Auth; + } + + // @ts-ignore + interface FastifyRequest { + pkgMetadata: pluginUtils.AuthPluginPackage; + } +} diff --git a/packages/server/fastify/src/plugins/storage.ts b/packages/server/fastify/src/plugins/storage.ts index 14b8807ce..4b32bb0d3 100644 --- a/packages/server/fastify/src/plugins/storage.ts +++ b/packages/server/fastify/src/plugins/storage.ts @@ -18,6 +18,7 @@ export default fp( ); declare module 'fastify' { + // @ts-ignore interface FastifyInstance { storage: Storage; } diff --git a/packages/server/fastify/src/plugins/userRemoteVerify.ts b/packages/server/fastify/src/plugins/userRemoteVerify.ts new file mode 100644 index 000000000..da7ad1d5b --- /dev/null +++ b/packages/server/fastify/src/plugins/userRemoteVerify.ts @@ -0,0 +1,27 @@ +import { FastifyInstance } from 'fastify'; +import fp from 'fastify-plugin'; + +import { Auth } from '@verdaccio/auth'; +import { createAnonymousRemoteUser } from '@verdaccio/config'; +import { Config as IConfig } from '@verdaccio/types'; + +export default fp( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async function (fastify: FastifyInstance, _opts: { config: IConfig; filters?: unknown }) { + // ensure user remote is populated on every request + fastify.addHook('onRequest', (request, _reply, done) => { + request.userRemote = createAnonymousRemoteUser(); + done(); + }); + }, + { + fastify: '>=4.x', + } +); + +declare module 'fastify' { + // @ts-ignore + interface FastifyInstance { + auth: Auth; + } +} diff --git a/packages/server/fastify/src/server.ts b/packages/server/fastify/src/server.ts index 5b0f0a23b..e31056f87 100644 --- a/packages/server/fastify/src/server.ts +++ b/packages/server/fastify/src/server.ts @@ -1,7 +1,7 @@ import buildDebug from 'debug'; import fastify from 'fastify'; -import { Config as AppConfig, createAnonymousRemoteUser } from '@verdaccio/config'; +import { Config as AppConfig } from '@verdaccio/config'; import { logger } from '@verdaccio/logger'; import { ConfigYaml, Config as IConfig, RemoteUser } from '@verdaccio/types'; @@ -16,6 +16,7 @@ import authPlugin from './plugins/auth'; import configPlugin from './plugins/config'; import coreUtils from './plugins/coreUtils'; import storagePlugin from './plugins/storage'; +import userRemoteVerify from './plugins/userRemoteVerify'; import login from './routes/web/api/login'; import readme from './routes/web/api/readme'; import sidebar from './routes/web/api/sidebar'; @@ -32,15 +33,11 @@ async function startServer(config: ConfigYaml): Promise { debug('start fastify server'); // TODO: custom logger type and logger accepted by fastify does not match const fastifyInstance = fastify({ logger: logger as any }); - fastifyInstance.addHook('onRequest', (request, reply, done) => { - request.userRemote = createAnonymousRemoteUser(); - done(); - }); fastifyInstance.register(coreUtils); fastifyInstance.register(configPlugin, { config }); fastifyInstance.register(storagePlugin, { config: configInstance }); fastifyInstance.register(authPlugin, { config: configInstance }); - + fastifyInstance.register(userRemoteVerify); // api fastifyInstance.register((instance, opts, done) => { instance.register(ping); @@ -57,17 +54,19 @@ async function startServer(config: ConfigYaml): Promise { done(); }); - // web - fastifyInstance.register((instance, opts, done) => { - instance.register(ping, { prefix: '/web' }); - done(); + // debugging purpose + fastifyInstance.addHook('onRoute', (routeOptions) => { + debug('route: prefix: %s url: %s', routeOptions.prefix, routeOptions.routePath); }); + return fastifyInstance; } declare module 'fastify' { + // @ts-ignore interface FastifyRequest { userRemote: RemoteUser; + authenticationHeader?: string; } }