Skip to content

Commit

Permalink
feat: allow override user rate limit and user agent (#2803)
Browse files Browse the repository at this point in the history
* feat: allow override user rate limit

- improve user_agent config allow string

* chore: fix tests

* chore: refactor userRateLimit

* chore: remove comment

* chore: optional prop

* chore: refactor limiter

* chore: refactor endpoints

* chore: fix undefined

* chore: fix params

* chore: fix params

* chore: update ui

* chore: refactor limiter

* chore: fix tests

* chore: fix test
  • Loading branch information
juanpicado committed Dec 24, 2021
1 parent f64e403 commit 5b1264c
Show file tree
Hide file tree
Showing 32 changed files with 192 additions and 179 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/e2e-jest-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ jobs:
with:
node-version: 12.x
- name: 'install latest npm'
run: npm i -g npm
run: npm i -g npm@next-7
- name: Install Dependencies
run: yarn install
- name: 'Run verdaccio in the background'
Expand All @@ -140,7 +140,7 @@ jobs:
yarn jest module.test.js
pnpm:
name: 'pnpm:jest example'
name: 'pnpm:latest:jest example'
runs-on: ubuntu-latest

steps:
Expand Down
20 changes: 10 additions & 10 deletions .pnp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@verdaccio/local-storage": "10.1.0",
"@verdaccio/readme": "10.2.0",
"@verdaccio/streams": "10.1.0",
"@verdaccio/ui-theme": "3.2.1",
"@verdaccio/ui-theme": "3.4.1",
"JSONStream": "1.3.5",
"async": "3.2.2",
"body-parser": "1.19.1",
Expand Down Expand Up @@ -104,7 +104,7 @@
"@typescript-eslint/eslint-plugin": "4.13.0",
"@typescript-eslint/parser": "4.13.0",
"@verdaccio/eslint-config": "^8.5.0",
"@verdaccio/types": "^9.7.2",
"@verdaccio/types": "10.2.2",
"all-contributors-cli": "6.20.0",
"babel-eslint": "10.1.0",
"babel-jest": "26.6.3",
Expand Down
9 changes: 4 additions & 5 deletions src/api/endpoint/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@ import { createRemoteUser, createSessionToken, getApiToken, getAuthenticatedMess
import { logger } from '../../../lib/logger';

import { $RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth } from '../../../../types';
import { limiter } from '../../user-rate-limit';
import { limiter } from '../../rate-limiter';

export default function (route: Router, auth: IAuth, config: Config): void {
/* eslint new-cap:off */
const userRouter = express.Router();
userRouter.use(limiter);

userRouter.get('/-/user/:org_couchdb_user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
userRouter.get('/-/user/:org_couchdb_user', limiter(config?.userRateLimit), function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
res.status(HTTP_STATUS.OK);
next({
ok: getAuthenticatedMessage(req.remote_user.name),
});
});

userRouter.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
userRouter.put('/-/user/:org_couchdb_user/:_rev?/:revision?', limiter(config?.userRateLimit), function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
const { name, password } = req.body;
const remoteName = req.remote_user.name;

Expand Down Expand Up @@ -74,7 +73,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
}
});

userRouter.delete('/-/user/token/*', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
userRouter.delete('/-/user/token/*', limiter(config?.userRateLimit), function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
res.status(HTTP_STATUS.OK);
next({
ok: API_MESSAGE.LOGGED_OUT,
Expand Down
5 changes: 1 addition & 4 deletions src/api/endpoint/api/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { Response, Router } from 'express';
import { limiter } from '../../../user-rate-limit';
import profile from './profile';
import token from './token';
import v1Search from './search';

export default (auth, storage, config) => {
const route = Router(); /* eslint new-cap: 0 */
route.use(limiter);
route.use('/-/npm/v1/', profile(auth));
route.use('/-/npm/v1/', profile(auth, config));
route.use('/-/npm/v1/', token(auth, storage, config));
return route;
};
7 changes: 4 additions & 3 deletions src/api/endpoint/api/v1/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ErrorCode } from '../../../../lib/utils';
import { validatePassword } from '../../../../lib/auth-utils';

import { $NextFunctionVer, $RequestExtend, IAuth } from '../../../../../types';
import { limiter } from '../../../rate-limiter';

export interface Profile {
tfa: boolean;
Expand All @@ -17,7 +18,7 @@ export interface Profile {
fullname: string;
}

export default function (auth: IAuth): Router {
export default function (auth: IAuth, config): Router {
const profileRoute = Router(); /* eslint new-cap: 0 */
function buildProfile(name: string): Profile {
return {
Expand All @@ -32,7 +33,7 @@ export default function (auth: IAuth): Router {
};
}

profileRoute.get('/user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
profileRoute.get('/user', limiter(config?.userRateLimit), function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name) === false) {
return next(buildProfile(req.remote_user.name));
}
Expand All @@ -43,7 +44,7 @@ export default function (auth: IAuth): Router {
});
});

profileRoute.post('/user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
profileRoute.post('/user', limiter(config?.userRateLimit), function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
if (_.isNil(req.remote_user.name)) {
res.status(HTTP_STATUS.UNAUTHORIZED);
return next({
Expand Down
9 changes: 4 additions & 5 deletions src/api/endpoint/api/v1/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { stringToMD5 } from '../../../../lib/crypto-utils';
import { logger } from '../../../../lib/logger';

import { $NextFunctionVer, $RequestExtend, IAuth, IStorageHandler } from '../../../../../types';
import { limiter } from '../../../user-rate-limit';
import { limiter } from '../../../rate-limiter';

const debug = buildDebug('verdaccio:token');
export type NormalizeToken = Token & {
Expand All @@ -26,8 +26,7 @@ function normalizeToken(token: Token): NormalizeToken {
// https://github.com/npm/npm-profile/blob/latest/lib/index.js
export default function (auth: IAuth, storage: IStorageHandler, config: Config): Router {
const tokenRoute = Router(); /* eslint new-cap: 0 */
// tokenRoute.use(limiter);
tokenRoute.get('/tokens', async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
tokenRoute.get('/tokens', limiter(config?.userRateLimit), async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { name } = req.remote_user;

if (_.isNil(name) === false) {
Expand All @@ -50,7 +49,7 @@ export default function (auth: IAuth, storage: IStorageHandler, config: Config):
return next(ErrorCode.getUnauthorized());
});

tokenRoute.post('/tokens', function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
tokenRoute.post('/tokens', limiter(config?.userRateLimit), function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
const { password, readonly, cidr_whitelist } = req.body;
const { name } = req.remote_user;

Expand Down Expand Up @@ -110,7 +109,7 @@ export default function (auth: IAuth, storage: IStorageHandler, config: Config):
});
});

tokenRoute.delete('/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
tokenRoute.delete('/tokens/token/:tokenKey', limiter(config?.userRateLimit), async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
const {
params: { tokenKey },
} = req;
Expand Down
2 changes: 1 addition & 1 deletion src/api/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler)
ping(app);
stars(app, storage);
v1Search(app, auth, storage);
app.use(npmV1(auth, storage, config));
user(app, auth, config);
app.use(npmV1(auth, storage, config));
return app;
}
4 changes: 2 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Config as IConfig, IPluginMiddleware, IPluginStorageFilter } from '@ver
import Storage from '../lib/storage';
import loadPlugin from '../lib/plugin-loader';
import Auth from '../lib/auth';
import { ErrorCode } from '../lib/utils';
import { ErrorCode, getUserAgent } from '../lib/utils';
import { API_ERROR, HTTP_STATUS } from '../lib/constants';
import AppConfig from '../lib/config';
import { $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler, IAuth } from '../../types';
Expand All @@ -32,7 +32,7 @@ const defineAPI = function (config: IConfig, storage: IStorageHandler): any {
app.use(errorReportingMiddleware);
if (config.user_agent) {
app.use(function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
res.setHeader('X-Powered-By', config.user_agent);
res.setHeader('X-Powered-By', getUserAgent(config.user_agent));
next();
});
} else {
Expand Down
11 changes: 11 additions & 0 deletions src/api/rate-limiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import RateLimit from 'express-rate-limit';
import { RateLimit as RateLimitType } from '@verdaccio/types';

const limiter = (rateLimitOptions: RateLimitType) => {
// @ts-ignore
return new RateLimit({
...rateLimitOptions,
});
};

export { limiter };
11 changes: 0 additions & 11 deletions src/api/user-rate-limit.ts

This file was deleted.

9 changes: 2 additions & 7 deletions src/api/web/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import { Config } from '@verdaccio/types';
import Search from '../../lib/search';
import { match, validateName, validatePackage, setSecurityWebHeaders } from '../middleware';
import { IAuth, IStorageHandler } from '../../../types';
import addUserAuthApi from './endpoint/user';
import addPackageWebApi from './endpoint/package';
import addSearchWebApi from './endpoint/search';
import webApi from './endpoint';

const route = Router(); /* eslint new-cap: 0 */

Expand All @@ -25,9 +23,6 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler):
route.use(bodyParser.urlencoded({ extended: false }));
route.use(auth.webUIJWTmiddleware());
route.use(setSecurityWebHeaders);

addPackageWebApi(route, storage, auth, config);
addSearchWebApi(route, storage, auth);
addUserAuthApi(route, auth, config);
route.use(webApi(auth, storage, config));
return route;
}
22 changes: 22 additions & 0 deletions src/api/web/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Response, Router } from 'express';
import { limiter } from '../../rate-limiter';
import packageApi from './package';
import search from './search';
import user from './user';

export default (auth, storage, config) => {
const route = Router(); /* eslint new-cap: 0 */
route.use(
'/data/',
limiter({
windowMs: 2 * 60 * 1000, // 2 minutes
max: 5000, // limit each IP to 1000 requests per windowMs
...config?.web?.rateLimit,
})
);
route.use('/data/', packageApi(storage, auth, config));
route.use('/data/', search(storage, auth));
route.use('/sec/', limiter(config?.userRateLimit));
route.use('/sec/', user(auth, storage));
return route;
};
15 changes: 9 additions & 6 deletions src/api/web/endpoint/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const getOrder = (order = 'asc') => {

export type PackcageExt = Package & { author: any; dist?: { tarball: string } };

function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth, config: Config): void {
function addPackageWebApi(storage: IStorageHandler, auth: IAuth, config: Config): Router {
const can = allow(auth);
const pkgRouter = Router(); /* eslint new-cap: 0 */

const checkAllow = (name, remoteUser): Promise<boolean> =>
new Promise((resolve, reject): void => {
Expand All @@ -43,7 +44,7 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,
});

// Get list of all visible package
route.get('/packages', function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
pkgRouter.get('/packages', function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
storage.getLocalDatabase(async function (err, packages): Promise<void> {
if (err) {
throw err;
Expand Down Expand Up @@ -87,7 +88,7 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,
});

// Get package readme
route.get('/package/readme/(@:scope/)?:package/:version?', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
pkgRouter.get('/package/readme/(@:scope/)?:package/:version?', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
const packageName = req.params.scope ? addScope(req.params.scope, req.params.package) : req.params.package;

storage.getPackage({
Expand All @@ -101,13 +102,13 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,

res.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_PLAIN);
const referer = req.get('Referer');
const pathname = referer ? (new URL(referer)).pathname : undefined;
next(parseReadme(info.name, info.readme, {pathname}));
const pathname = referer ? new URL(referer).pathname : undefined;
next(parseReadme(info.name, info.readme, { pathname }));
},
});
});

route.get('/sidebar/(@:scope/)?:package', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
pkgRouter.get('/sidebar/(@:scope/)?:package', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
const packageName: string = req.params.scope ? addScope(req.params.scope, req.params.package) : req.params.package;

storage.getPackage({
Expand Down Expand Up @@ -148,6 +149,8 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,
},
});
});

return pkgRouter;
}

export default addPackageWebApi;
Loading

0 comments on commit 5b1264c

Please sign in to comment.