mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-04-01 02:42:23 -05:00
* feat: add rate limit to user api endpoints * chore: fix test * chore: refactor token endpoint * chore: refactor
This commit is contained in:
parent
c91d6beb8b
commit
f64e403f0a
8 changed files with 88 additions and 88 deletions
src/api
|
@ -2,23 +2,28 @@ import _ from 'lodash';
|
|||
import Cookies from 'cookies';
|
||||
|
||||
import { Config, RemoteUser } from '@verdaccio/types';
|
||||
import { Response, Router } from 'express';
|
||||
import express, { Response, Router } from 'express';
|
||||
import { ErrorCode } from '../../../lib/utils';
|
||||
import { API_ERROR, API_MESSAGE, HEADERS, HTTP_STATUS } from '../../../lib/constants';
|
||||
import { createRemoteUser, createSessionToken, getApiToken, getAuthenticatedMessage, validatePassword } from '../../../lib/auth-utils';
|
||||
import { logger } from '../../../lib/logger';
|
||||
|
||||
import { $RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth } from '../../../../types';
|
||||
import { limiter } from '../../user-rate-limit';
|
||||
|
||||
export default function (route: Router, auth: IAuth, config: Config): void {
|
||||
route.get('/-/user/:org_couchdb_user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): 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 {
|
||||
res.status(HTTP_STATUS.OK);
|
||||
next({
|
||||
ok: getAuthenticatedMessage(req.remote_user.name),
|
||||
});
|
||||
});
|
||||
|
||||
route.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
userRouter.put('/-/user/:org_couchdb_user/:_rev?/:revision?', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
const { name, password } = req.body;
|
||||
const remoteName = req.remote_user.name;
|
||||
|
||||
|
@ -69,7 +74,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
|
|||
}
|
||||
});
|
||||
|
||||
route.delete('/-/user/token/*', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
userRouter.delete('/-/user/token/*', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
res.status(HTTP_STATUS.OK);
|
||||
next({
|
||||
ok: API_MESSAGE.LOGGED_OUT,
|
||||
|
@ -78,7 +83,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
|
|||
|
||||
// placeholder 'cause npm require to be authenticated to publish
|
||||
// we do not do any real authentication yet
|
||||
route.post('/_session', Cookies.express(), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
userRouter.post('/_session', Cookies.express(), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
res.cookies.set('AuthSession', String(Math.random()), createSessionToken());
|
||||
|
||||
next({
|
||||
|
@ -87,4 +92,6 @@ export default function (route: Router, auth: IAuth, config: Config): void {
|
|||
roles: [],
|
||||
});
|
||||
});
|
||||
|
||||
route.use(userRouter);
|
||||
}
|
||||
|
|
13
src/api/endpoint/api/v1/index.ts
Normal file
13
src/api/endpoint/api/v1/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
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/', token(auth, storage, config));
|
||||
return route;
|
||||
};
|
|
@ -17,7 +17,8 @@ export interface Profile {
|
|||
fullname: string;
|
||||
}
|
||||
|
||||
export default function (route: Router, auth: IAuth): void {
|
||||
export default function (auth: IAuth): Router {
|
||||
const profileRoute = Router(); /* eslint new-cap: 0 */
|
||||
function buildProfile(name: string): Profile {
|
||||
return {
|
||||
tfa: false,
|
||||
|
@ -27,68 +28,55 @@ export default function (route: Router, auth: IAuth): void {
|
|||
created: '',
|
||||
updated: '',
|
||||
cidr_whitelist: null,
|
||||
fullname: ''
|
||||
fullname: '',
|
||||
};
|
||||
}
|
||||
|
||||
route.get(
|
||||
'/-/npm/v1/user',
|
||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
if (_.isNil(req.remote_user.name) === false) {
|
||||
return next(buildProfile(req.remote_user.name));
|
||||
}
|
||||
profileRoute.get('/user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
if (_.isNil(req.remote_user.name) === false) {
|
||||
return next(buildProfile(req.remote_user.name));
|
||||
}
|
||||
|
||||
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||
return next({
|
||||
message: API_ERROR.MUST_BE_LOGGED,
|
||||
});
|
||||
});
|
||||
|
||||
profileRoute.post('/user', function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
if (_.isNil(req.remote_user.name)) {
|
||||
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||
return next({
|
||||
message: API_ERROR.MUST_BE_LOGGED
|
||||
message: API_ERROR.MUST_BE_LOGGED,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/-/npm/v1/user',
|
||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||
if (_.isNil(req.remote_user.name)) {
|
||||
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||
return next({
|
||||
message: API_ERROR.MUST_BE_LOGGED
|
||||
});
|
||||
const { password, tfa } = req.body;
|
||||
const { name } = req.remote_user;
|
||||
|
||||
if (_.isNil(password) === false) {
|
||||
if (validatePassword(password.new) === false) {
|
||||
/* eslint new-cap:off */
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT()));
|
||||
/* eslint new-cap:off */
|
||||
}
|
||||
|
||||
const { password, tfa } = req.body;
|
||||
const { name } = req.remote_user;
|
||||
|
||||
if (_.isNil(password) === false) {
|
||||
if (validatePassword(password.new) === false) {
|
||||
/* eslint new-cap:off */
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, API_ERROR.PASSWORD_SHORT()));
|
||||
/* eslint new-cap:off */
|
||||
auth.changePassword(name, password.old, password.new, (err, isUpdated): $NextFunctionVer => {
|
||||
if (_.isNull(err) === false) {
|
||||
return next(ErrorCode.getCode(err.status, err.message) || ErrorCode.getConflict(err.message));
|
||||
}
|
||||
|
||||
auth.changePassword(
|
||||
name,
|
||||
password.old,
|
||||
password.new,
|
||||
(err, isUpdated): $NextFunctionVer => {
|
||||
if (_.isNull(err) === false) {
|
||||
return next(
|
||||
ErrorCode.getCode(err.status, err.message) || ErrorCode.getConflict(err.message)
|
||||
);
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
return next(buildProfile(req.remote_user.name));
|
||||
}
|
||||
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
);
|
||||
} else if (_.isNil(tfa) === false) {
|
||||
return next(
|
||||
ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED)
|
||||
);
|
||||
} else {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));
|
||||
}
|
||||
if (isUpdated) {
|
||||
return next(buildProfile(req.remote_user.name));
|
||||
}
|
||||
return next(ErrorCode.getInternalError(API_ERROR.INTERNAL_SERVER_ERROR));
|
||||
});
|
||||
} else if (_.isNil(tfa) === false) {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.SERVICE_UNAVAILABLE, SUPPORT_ERRORS.TFA_DISABLED));
|
||||
} else {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.INTERNAL_ERROR, APP_ERROR.PROFILE_ERROR));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return profileRoute;
|
||||
}
|
||||
|
|
|
@ -9,6 +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';
|
||||
|
||||
const debug = buildDebug('verdaccio:token');
|
||||
export type NormalizeToken = Token & {
|
||||
|
@ -23,8 +24,10 @@ function normalizeToken(token: Token): NormalizeToken {
|
|||
}
|
||||
|
||||
// https://github.com/npm/npm-profile/blob/latest/lib/index.js
|
||||
export default function (route: Router, auth: IAuth, storage: IStorageHandler, config: Config): void {
|
||||
route.get('/-/npm/v1/tokens', async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||
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) {
|
||||
const { name } = req.remote_user;
|
||||
|
||||
if (_.isNil(name) === false) {
|
||||
|
@ -47,7 +50,7 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
|
|||
return next(ErrorCode.getUnauthorized());
|
||||
});
|
||||
|
||||
route.post('/-/npm/v1/tokens', function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||
tokenRoute.post('/tokens', function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||
const { password, readonly, cidr_whitelist } = req.body;
|
||||
const { name } = req.remote_user;
|
||||
|
||||
|
@ -107,7 +110,7 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
|
|||
});
|
||||
});
|
||||
|
||||
route.delete('/-/npm/v1/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
|
||||
tokenRoute.delete('/tokens/token/:tokenKey', async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
|
||||
const {
|
||||
params: { tokenKey },
|
||||
} = req;
|
||||
|
@ -126,4 +129,6 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
|
|||
}
|
||||
return next(ErrorCode.getUnauthorized());
|
||||
});
|
||||
|
||||
return tokenRoute;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,7 @@ import publish from './api/publish';
|
|||
import search from './api/search';
|
||||
import pkg from './api/package';
|
||||
import stars from './api/stars';
|
||||
import profile from './api/v1/profile';
|
||||
import token from './api/v1/token';
|
||||
import npmV1 from './api/v1';
|
||||
import v1Search from './api/v1/search';
|
||||
|
||||
const { match, validateName, validatePackage, encodeScopePackage, antiLoop } = require('../middleware');
|
||||
|
@ -44,14 +43,13 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler)
|
|||
// for "npm whoami"
|
||||
whoami(app);
|
||||
pkg(app, auth, storage, config);
|
||||
profile(app, auth);
|
||||
search(app, auth, storage);
|
||||
user(app, auth, config);
|
||||
distTags(app, auth, storage);
|
||||
publish(app, auth, storage, config);
|
||||
ping(app);
|
||||
stars(app, storage);
|
||||
v1Search(app, auth, storage);
|
||||
token(app, auth, storage, config);
|
||||
app.use(npmV1(auth, storage, config));
|
||||
user(app, auth, config);
|
||||
return app;
|
||||
}
|
||||
|
|
11
src/api/user-rate-limit.ts
Normal file
11
src/api/user-rate-limit.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import RateLimit from 'express-rate-limit';
|
||||
|
||||
// we limit max 1000 request per 15 minutes on user endpoints
|
||||
const defaultUserRateLimiting = {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000,
|
||||
};
|
||||
// @ts-ignore
|
||||
const limiter = new RateLimit(defaultUserRateLimiting);
|
||||
|
||||
export { limiter };
|
|
@ -15,12 +15,9 @@ const route = Router(); /* eslint new-cap: 0 */
|
|||
*/
|
||||
export default function (config: Config, auth: IAuth, storage: IStorageHandler): Router {
|
||||
Search.configureStorage(storage);
|
||||
|
||||
// validate all of these params as a package name
|
||||
// this might be too harsh, so ask if it causes trouble
|
||||
// $FlowFixMe
|
||||
route.param('package', validatePackage);
|
||||
// $FlowFixMe
|
||||
route.param('filename', validateName);
|
||||
route.param('version', validateName);
|
||||
route.param('anything', match(/.*/));
|
||||
|
@ -32,11 +29,5 @@ export default function (config: Config, auth: IAuth, storage: IStorageHandler):
|
|||
addPackageWebApi(route, storage, auth, config);
|
||||
addSearchWebApi(route, storage, auth);
|
||||
addUserAuthApi(route, auth, config);
|
||||
|
||||
// What are you looking for? logout? client side will remove token when user click logout,
|
||||
// or it will auto expire after 24 hours.
|
||||
// This token is different with the token send to npm client.
|
||||
// We will/may replace current token with JWT in next major release, and it will not expire at all(configurable).
|
||||
|
||||
return route;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
* @prettier
|
||||
*/
|
||||
import _ from 'lodash';
|
||||
import RateLimit from 'express-rate-limit';
|
||||
|
||||
import express, { Router, Response, Request } from 'express';
|
||||
import { Config, RemoteUser, JWTSignOptions } from '@verdaccio/types';
|
||||
|
@ -10,19 +9,9 @@ import { API_ERROR, APP_ERROR, HEADERS, HTTP_STATUS } from '../../../lib/constan
|
|||
import { IAuth, $NextFunctionVer } from '../../../../types';
|
||||
import { ErrorCode } from '../../../lib/utils';
|
||||
import { getSecurity, validatePassword } from '../../../lib/auth-utils';
|
||||
import { limiter } from '../../user-rate-limit';
|
||||
|
||||
function addUserAuthApi(route: Router, auth: IAuth, config: Config): void {
|
||||
/* eslint new-cap:off */
|
||||
const userRouter = express.Router();
|
||||
|
||||
// we limit max 100 request per 15 minutes on user endpoints
|
||||
// @ts-ignore
|
||||
const limiter = new RateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100,
|
||||
// @ts-ignore
|
||||
...config?.web?.rateLimit,
|
||||
});
|
||||
route.use(limiter);
|
||||
route.post('/login', function (req: Request, res: Response, next: $NextFunctionVer): void {
|
||||
const { username, password } = req.body;
|
||||
|
@ -69,8 +58,6 @@ function addUserAuthApi(route: Router, auth: IAuth, config: Config): void {
|
|||
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, APP_ERROR.PASSWORD_VALIDATION));
|
||||
}
|
||||
});
|
||||
|
||||
route.use(userRouter);
|
||||
}
|
||||
|
||||
export default addUserAuthApi;
|
||||
|
|
Loading…
Add table
Reference in a new issue