mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-11-08 23:25:51 +01:00
feat: allow override user rate limit and user agent (#2803)
* 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
This commit is contained in:
parent
f64e403f0a
commit
5b1264c733
4
.github/workflows/e2e-jest-workflow.yml
vendored
4
.github/workflows/e2e-jest-workflow.yml
vendored
@ -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'
|
||||
@ -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:
|
||||
|
20
.pnp.js
generated
20
.pnp.js
generated
@ -83,8 +83,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@verdaccio/local-storage", "npm:10.1.0"],
|
||||
["@verdaccio/readme", "npm:10.2.0"],
|
||||
["@verdaccio/streams", "npm:10.1.0"],
|
||||
["@verdaccio/types", "npm:9.7.2"],
|
||||
["@verdaccio/ui-theme", "npm:3.2.1"],
|
||||
["@verdaccio/types", "npm:10.2.2"],
|
||||
["@verdaccio/ui-theme", "npm:3.4.1"],
|
||||
["JSONStream", "npm:1.3.5"],
|
||||
["all-contributors-cli", "npm:6.20.0"],
|
||||
["async", "npm:3.2.2"],
|
||||
@ -5397,19 +5397,19 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]
|
||||
]],
|
||||
["@verdaccio/types", [
|
||||
["npm:9.7.2", {
|
||||
"packageLocation": "./.yarn/cache/@verdaccio-types-npm-9.7.2-9a89bfb123-ad04a1cd1f.zip/node_modules/@verdaccio/types/",
|
||||
["npm:10.2.2", {
|
||||
"packageLocation": "./.yarn/cache/@verdaccio-types-npm-10.2.2-3a8a5ff733-cd16cad480.zip/node_modules/@verdaccio/types/",
|
||||
"packageDependencies": [
|
||||
["@verdaccio/types", "npm:9.7.2"]
|
||||
["@verdaccio/types", "npm:10.2.2"]
|
||||
],
|
||||
"linkType": "HARD",
|
||||
}]
|
||||
]],
|
||||
["@verdaccio/ui-theme", [
|
||||
["npm:3.2.1", {
|
||||
"packageLocation": "./.yarn/cache/@verdaccio-ui-theme-npm-3.2.1-86c131bd8f-bd0c23b2ff.zip/node_modules/@verdaccio/ui-theme/",
|
||||
["npm:3.4.1", {
|
||||
"packageLocation": "./.yarn/cache/@verdaccio-ui-theme-npm-3.4.1-ff9f347e40-9873985e77.zip/node_modules/@verdaccio/ui-theme/",
|
||||
"packageDependencies": [
|
||||
["@verdaccio/ui-theme", "npm:3.2.1"]
|
||||
["@verdaccio/ui-theme", "npm:3.4.1"]
|
||||
],
|
||||
"linkType": "HARD",
|
||||
}]
|
||||
@ -17240,8 +17240,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@verdaccio/local-storage", "npm:10.1.0"],
|
||||
["@verdaccio/readme", "npm:10.2.0"],
|
||||
["@verdaccio/streams", "npm:10.1.0"],
|
||||
["@verdaccio/types", "npm:9.7.2"],
|
||||
["@verdaccio/ui-theme", "npm:3.2.1"],
|
||||
["@verdaccio/types", "npm:10.2.2"],
|
||||
["@verdaccio/ui-theme", "npm:3.4.1"],
|
||||
["JSONStream", "npm:1.3.5"],
|
||||
["all-contributors-cli", "npm:6.20.0"],
|
||||
["async", "npm:3.2.2"],
|
||||
|
BIN
.yarn/cache/@verdaccio-types-npm-10.2.2-3a8a5ff733-cd16cad480.zip
vendored
Normal file
BIN
.yarn/cache/@verdaccio-types-npm-10.2.2-3a8a5ff733-cd16cad480.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@verdaccio-ui-theme-npm-3.4.1-ff9f347e40-9873985e77.zip
vendored
Normal file
BIN
.yarn/cache/@verdaccio-ui-theme-npm-3.4.1-ff9f347e40-9873985e77.zip
vendored
Normal file
Binary file not shown.
@ -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",
|
||||
@ -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",
|
||||
|
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
@ -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 {
|
||||
@ -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));
|
||||
}
|
||||
@ -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({
|
||||
|
@ -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 & {
|
||||
@ -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) {
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
@ -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 {
|
||||
|
11
src/api/rate-limiter.ts
Normal file
11
src/api/rate-limiter.ts
Normal file
@ -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 };
|
@ -1,11 +0,0 @@
|
||||
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 };
|
@ -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 */
|
||||
|
||||
@ -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
src/api/web/endpoint/index.ts
Normal file
22
src/api/web/endpoint/index.ts
Normal file
@ -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;
|
||||
};
|
@ -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 => {
|
||||
@ -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;
|
||||
@ -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({
|
||||
@ -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({
|
||||
@ -148,6 +149,8 @@ function addPackageWebApi(route: Router, storage: IStorageHandler, auth: IAuth,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return pkgRouter;
|
||||
}
|
||||
|
||||
export default addPackageWebApi;
|
||||
|
@ -7,58 +7,48 @@ import { Router } from 'express';
|
||||
import { Package } from '@verdaccio/types';
|
||||
import Search from '../../../lib/search';
|
||||
import { DIST_TAGS } from '../../../lib/constants';
|
||||
import {
|
||||
IAuth,
|
||||
$ResponseExtend,
|
||||
$RequestExtend,
|
||||
$NextFunctionVer,
|
||||
IStorageHandler
|
||||
} from '../../../../types';
|
||||
import { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler } from '../../../../types';
|
||||
|
||||
function addSearchWebApi(route: Router, storage: IStorageHandler, auth: IAuth): void {
|
||||
function addSearchWebApi(storage: IStorageHandler, auth: IAuth): Router {
|
||||
const route = Router(); /* eslint new-cap: 0 */
|
||||
// Search package
|
||||
route.get(
|
||||
'/search/:anything',
|
||||
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
const results: any = Search.query(req.params.anything);
|
||||
// FUTURE: figure out here the correct type
|
||||
const packages: any[] = [];
|
||||
route.get('/search/:anything', function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||
const results: any = Search.query(req.params.anything);
|
||||
// FUTURE: figure out here the correct type
|
||||
const packages: any[] = [];
|
||||
|
||||
const getPackageInfo = function (i): void {
|
||||
storage.getPackage({
|
||||
name: results[i].ref,
|
||||
uplinksLook: false,
|
||||
callback: (err, entry: Package): void => {
|
||||
if (!err && entry) {
|
||||
auth.allow_access(
|
||||
{ packageName: entry.name },
|
||||
req.remote_user,
|
||||
function (err, allowed): void {
|
||||
if (err || !allowed) {
|
||||
return;
|
||||
}
|
||||
const getPackageInfo = function (i): void {
|
||||
storage.getPackage({
|
||||
name: results[i].ref,
|
||||
uplinksLook: false,
|
||||
callback: (err, entry: Package): void => {
|
||||
if (!err && entry) {
|
||||
auth.allow_access({ packageName: entry.name }, req.remote_user, function (err, allowed): void {
|
||||
if (err || !allowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
packages.push(entry.versions[entry[DIST_TAGS].latest]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (i >= results.length - 1) {
|
||||
next(packages);
|
||||
} else {
|
||||
getPackageInfo(i + 1);
|
||||
}
|
||||
packages.push(entry.versions[entry[DIST_TAGS].latest]);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (results.length) {
|
||||
getPackageInfo(0);
|
||||
} else {
|
||||
next([]);
|
||||
}
|
||||
if (i >= results.length - 1) {
|
||||
next(packages);
|
||||
} else {
|
||||
getPackageInfo(i + 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (results.length) {
|
||||
getPackageInfo(0);
|
||||
} else {
|
||||
next([]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
export default addSearchWebApi;
|
||||
|
@ -9,10 +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 {
|
||||
route.use(limiter);
|
||||
function addUserAuthApi(auth: IAuth, config: Config): Router {
|
||||
const route = Router(); /* eslint new-cap: 0 */
|
||||
route.post('/login', function (req: Request, res: Response, next: $NextFunctionVer): void {
|
||||
const { username, password } = req.body;
|
||||
|
||||
@ -58,6 +57,8 @@ function addUserAuthApi(route: Router, auth: IAuth, config: Config): void {
|
||||
return next(ErrorCode.getCode(HTTP_STATUS.BAD_REQUEST, APP_ERROR.PASSWORD_VALIDATION));
|
||||
}
|
||||
});
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
export default addUserAuthApi;
|
||||
|
@ -4,7 +4,8 @@ import _ from 'lodash';
|
||||
|
||||
import express from 'express';
|
||||
import buildDebug from 'debug';
|
||||
import RateLimit from 'express-rate-limit';
|
||||
|
||||
import { Config } from '@verdaccio/types';
|
||||
|
||||
import Search from '../../lib/search';
|
||||
import { HTTP_STATUS } from '../../lib/constants';
|
||||
@ -54,23 +55,15 @@ const sendFileCallback = (next) => (err) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default function (config, auth, storage) {
|
||||
export default function (config: Config, auth, storage) {
|
||||
let { staticPath, manifest, manifestFiles } = loadTheme(config) || require('@verdaccio/ui-theme')();
|
||||
debug('static path %o', staticPath);
|
||||
Search.configureStorage(storage);
|
||||
|
||||
/* eslint new-cap:off */
|
||||
const router = express.Router();
|
||||
// limit 5k request on web peer 2 minutes is enough for a medium size company
|
||||
// @ts-ignore
|
||||
const limiter = new RateLimit({
|
||||
windowMs: 2 * 60 * 1000, // 2 minutes
|
||||
max: 5000, // limit each IP to 1000 requests per windowMs
|
||||
...config?.web?.rateLimit,
|
||||
});
|
||||
// run in production mode by default, just in case
|
||||
// it shouldn't make any difference anyway
|
||||
router.use(limiter);
|
||||
router.use(auth.webUIJWTmiddleware());
|
||||
router.use(setSecurityWebHeaders);
|
||||
|
||||
@ -93,6 +86,7 @@ export default function (config, auth, storage) {
|
||||
// Use POSIX version `path.posix.join` instead.
|
||||
config.web.logo = path.posix.join('/-/static/', path.basename(config.web.logo));
|
||||
router.get(config.web.logo, function (_req, res, next) {
|
||||
// @ts-ignore
|
||||
debug('serve custom logo web:%s - local:%s', config.web.logo, absoluteLocalFile);
|
||||
res.sendFile(absoluteLocalFile, sendFileCallback(next));
|
||||
});
|
||||
|
@ -125,6 +125,12 @@ const defaultApiTokenConf: APITokenOptions = {
|
||||
legacy: true,
|
||||
};
|
||||
|
||||
// we limit max 1000 request per 15 minutes on user endpoints
|
||||
export const defaultUserRateLimiting = {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000,
|
||||
};
|
||||
|
||||
export const defaultSecurity: Security = {
|
||||
web: defaultWebTokenOptions,
|
||||
api: defaultApiTokenConf,
|
||||
@ -154,7 +160,6 @@ export function isAESLegacy(security: Security): boolean {
|
||||
|
||||
export async function getApiToken(auth: IAuthWebUI, config: Config, remoteUser: RemoteUser, aesPassword: string): Promise<string> {
|
||||
const security: Security = getSecurity(config);
|
||||
|
||||
if (isAESLegacy(security)) {
|
||||
// fallback all goes to AES encryption
|
||||
return await new Promise((resolve): void => {
|
||||
|
@ -1,12 +1,13 @@
|
||||
import assert from 'assert';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { PackageList, Config as AppConfig, Security, Logger } from '@verdaccio/types';
|
||||
import { PackageList, Config as AppConfig, Security, Logger, RateLimit } from '@verdaccio/types';
|
||||
import { MatchedPackage, StartUpConfig } from '../../types';
|
||||
import { generateRandomHexString } from './crypto-utils';
|
||||
import { getMatchedPackagesSpec, normalisePackageAccess, sanityCheckUplinksProps, uplinkSanityCheck } from './config-utils';
|
||||
import { getUserAgent, isObject } from './utils';
|
||||
import { APP_ERROR } from './constants';
|
||||
import { defaultUserRateLimiting } from './auth-utils';
|
||||
|
||||
const LoggerApi = require('./logger');
|
||||
const strategicConfigProps = ['uplinks', 'packages'];
|
||||
@ -18,12 +19,13 @@ const allowedEnvConfig = ['http_proxy', 'https_proxy', 'no_proxy'];
|
||||
class Config implements AppConfig {
|
||||
public logger: Logger;
|
||||
// @ts-ignore
|
||||
public user_agent: string;
|
||||
public user_agent: boolean | string;
|
||||
// @ts-ignore
|
||||
public secret: string;
|
||||
public uplinks: any;
|
||||
public packages: PackageList;
|
||||
public users: any;
|
||||
public userRateLimit: RateLimit;
|
||||
public server_id: string;
|
||||
public self_path: string;
|
||||
public storage: string | void;
|
||||
@ -44,11 +46,12 @@ class Config implements AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (config?.user_agent) {
|
||||
this.user_agent = getUserAgent();
|
||||
this.user_agent = getUserAgent(config?.user_agent);
|
||||
}
|
||||
|
||||
this.userRateLimit = { ...defaultUserRateLimiting, ...config?.userRateLimit };
|
||||
|
||||
// some weird shell scripts are valid yaml files parsed as string
|
||||
assert(_.isObject(config), APP_ERROR.CONFIG_NOT_VALID);
|
||||
|
||||
|
@ -63,6 +63,7 @@ class ProxyStorage implements IProxy {
|
||||
public constructor(config: UpLinkConfLocal, mainConfig: Config) {
|
||||
this.config = config;
|
||||
this.failed_requests = 0;
|
||||
// @ts-ignore
|
||||
this.userAgent = mainConfig.user_agent;
|
||||
this.ca = config.ca;
|
||||
this.logger = logger;
|
||||
|
@ -30,9 +30,17 @@ const pkgVersion = module.exports.version;
|
||||
const pkgName = module.exports.name;
|
||||
const validProtocols = ['https', 'http'];
|
||||
|
||||
export function getUserAgent(): string {
|
||||
export function getUserAgent(customUserAgent?: boolean | string): string {
|
||||
assert(_.isString(pkgName));
|
||||
assert(_.isString(pkgVersion));
|
||||
if (customUserAgent === true) {
|
||||
return `${pkgName}/${pkgVersion}`;
|
||||
} else if (_.isString(customUserAgent) && _.isEmpty(customUserAgent) === false) {
|
||||
return customUserAgent;
|
||||
} else if (customUserAgent === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${pkgName}/${pkgVersion}`;
|
||||
}
|
||||
|
||||
@ -457,21 +465,19 @@ export function addGravatarSupport(pkgInfo: Package, online = true): AuthorAvata
|
||||
|
||||
// for contributors
|
||||
if (_.isEmpty(contributors) === false) {
|
||||
pkgInfoCopy.latest.contributors = contributors.map(
|
||||
(contributor): AuthorAvatar => {
|
||||
if (isObject(contributor)) {
|
||||
contributor.avatar = generateGravatarUrl(contributor.email, online);
|
||||
} else if (_.isString(contributor)) {
|
||||
contributor = {
|
||||
avatar: GENERIC_AVATAR,
|
||||
email: contributor,
|
||||
name: contributor,
|
||||
};
|
||||
}
|
||||
|
||||
return contributor;
|
||||
pkgInfoCopy.latest.contributors = contributors.map((contributor): AuthorAvatar => {
|
||||
if (isObject(contributor)) {
|
||||
contributor.avatar = generateGravatarUrl(contributor.email, online);
|
||||
} else if (_.isString(contributor)) {
|
||||
contributor = {
|
||||
avatar: GENERIC_AVATAR,
|
||||
email: contributor,
|
||||
name: contributor,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return contributor;
|
||||
});
|
||||
}
|
||||
|
||||
// for maintainers
|
||||
@ -492,9 +498,7 @@ export function addGravatarSupport(pkgInfo: Package, online = true): AuthorAvata
|
||||
* @param {Object} options sanitizyReadme options
|
||||
* @return {String} converted html template
|
||||
*/
|
||||
export function parseReadme(packageName: string,
|
||||
readme: string,
|
||||
options: { pathname?: string | void } = {}): string | void {
|
||||
export function parseReadme(packageName: string, readme: string, options: { pathname?: string | void } = {}): string | void {
|
||||
if (_.isEmpty(readme) === false) {
|
||||
return sanitizyReadme(readme, options);
|
||||
}
|
||||
|
@ -6,13 +6,13 @@ describe('/ (Verdaccio Page)', () => {
|
||||
// this might be increased based on the delays included in all test
|
||||
jest.setTimeout(20000);
|
||||
|
||||
const clickElement = async function(selector, options = { delay: 100 }) {
|
||||
const clickElement = async function (selector, options = { delay: 100 }) {
|
||||
const button = await page.$(selector);
|
||||
await button.focus();
|
||||
await button.click(options);
|
||||
};
|
||||
|
||||
const evaluateSignIn = async function(matchText = 'Login') {
|
||||
const evaluateSignIn = async function (matchText = 'Login') {
|
||||
const text = await page.evaluate(() => {
|
||||
return document.querySelector('button[data-testid="header--button-login"]').textContent;
|
||||
});
|
||||
@ -20,11 +20,11 @@ describe('/ (Verdaccio Page)', () => {
|
||||
expect(text).toMatch(matchText);
|
||||
};
|
||||
|
||||
const getPackages = async function() {
|
||||
const getPackages = async function () {
|
||||
return await page.$$('.package-title');
|
||||
};
|
||||
|
||||
const logIn = async function() {
|
||||
const logIn = async function () {
|
||||
await clickElement('button[data-testid="header--button-login"]');
|
||||
// we fill the sign in form
|
||||
const signInDialog = await page.$('#login--dialog');
|
||||
@ -47,7 +47,7 @@ describe('/ (Verdaccio Page)', () => {
|
||||
page = await global.__BROWSER__.newPage();
|
||||
await page.goto('http://0.0.0.0:55558');
|
||||
// eslint-disable-next-line no-console
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -162,7 +162,7 @@ describe('/ (Verdaccio Page)', () => {
|
||||
await page.waitFor(1000);
|
||||
const tags = await page.$$('.dep-tag');
|
||||
const tag = tags[0];
|
||||
const label = await page.evaluate(el => el.innerText, tag);
|
||||
const label = await page.evaluate((el) => el.innerText, tag);
|
||||
expect(label).toMatch('verdaccio@');
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,7 @@ const mkdirp = require('mkdirp');
|
||||
|
||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||
|
||||
module.exports = async function() {
|
||||
module.exports = async function () {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(green('Setup Puppeteer'));
|
||||
const browser = await puppeteer.launch({ headless: true, /* slowMo: 300 */ args: ['--no-sandbox'] });
|
||||
|
@ -51,7 +51,6 @@ export class PromiseAssert extends Promise<any> implements IRequestPromise {
|
||||
}
|
||||
|
||||
public body_error(expected: any) {
|
||||
// $FlowFixMe
|
||||
const selfData = this[requestData];
|
||||
|
||||
return injectResponse(
|
||||
@ -97,7 +96,6 @@ export class PromiseAssert extends Promise<any> implements IRequestPromise {
|
||||
}
|
||||
|
||||
function injectResponse(smartObject: any, promise: Promise<any>): Promise<any> {
|
||||
// $FlowFixMe
|
||||
promise[requestData] = smartObject[requestData];
|
||||
return promise;
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import { mockServer } from '../../__helper/mock';
|
||||
import { DOMAIN_SERVERS } from '../../../functional/config.functional';
|
||||
import { getNewToken } from '../../__helper/api';
|
||||
import { buildToken } from '../../../../src/lib/utils';
|
||||
import { expectJson } from '../../../../src/api/middleware';
|
||||
|
||||
require('../../../../src/lib/logger').setup([{ type: 'stdout', format: 'pretty', level: 'trace' }]);
|
||||
|
||||
|
@ -62,7 +62,7 @@ describe('endpoint web unit test', () => {
|
||||
describe('Packages', () => {
|
||||
test('should display all packages', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/packages')
|
||||
.get('/-/verdaccio/data/packages')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function (err, res) {
|
||||
expect(res.body).toHaveLength(1);
|
||||
@ -72,7 +72,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test.skip('should display scoped readme', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/package/readme/@scope/pk1-test')
|
||||
.get('/-/verdaccio/data/package/readme/@scope/pk1-test')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_CHARSET)
|
||||
.end(function (err, res) {
|
||||
@ -84,7 +84,7 @@ describe('endpoint web unit test', () => {
|
||||
// FIXME: disabled, we need to inspect why fails randomly
|
||||
test.skip('should display scoped readme 404', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/package/readme/@scope/404')
|
||||
.get('/-/verdaccio/data/package/readme/@scope/404')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_CHARSET)
|
||||
.end(function (err, res) {
|
||||
@ -95,7 +95,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should display sidebar info', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/sidebar/@scope/pk1-test')
|
||||
.get('/-/verdaccio/data/sidebar/@scope/pk1-test')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.end(function (err, res) {
|
||||
@ -112,7 +112,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should display sidebar info by version', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/sidebar/@scope/pk1-test?v=1.0.6')
|
||||
.get('/-/verdaccio/data/sidebar/@scope/pk1-test?v=1.0.6')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.end(function (err, res) {
|
||||
@ -129,7 +129,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should display sidebar info 404', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/sidebar/@scope/404')
|
||||
.get('/-/verdaccio/data/sidebar/@scope/404')
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.end(function () {
|
||||
@ -139,7 +139,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should display sidebar info 404 with version', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/sidebar/@scope/pk1-test?v=0.0.0-not-found')
|
||||
.get('/-/verdaccio/data/sidebar/@scope/pk1-test?v=0.0.0-not-found')
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
|
||||
.end(function () {
|
||||
@ -151,7 +151,7 @@ describe('endpoint web unit test', () => {
|
||||
describe('Search', () => {
|
||||
test('should search pk1-test', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/search/scope')
|
||||
.get('/-/verdaccio/data/search/scope')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function (err, res) {
|
||||
expect(res.body).toHaveLength(1);
|
||||
@ -161,7 +161,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should search with 404', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/search/@')
|
||||
.get('/-/verdaccio/data/search/@')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function (err, res) {
|
||||
// in a normal world, the output would be 1
|
||||
@ -173,7 +173,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should not find forbidden-place', (done) => {
|
||||
request(app)
|
||||
.get('/-/verdaccio/search/forbidden-place')
|
||||
.get('/-/verdaccio/data/search/forbidden-place')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.end(function (err, res) {
|
||||
// this is expected since we are not logged
|
||||
@ -192,7 +192,7 @@ describe('endpoint web unit test', () => {
|
||||
describe('login webui', () => {
|
||||
test('should log successfully', (done) => {
|
||||
request(app)
|
||||
.post('/-/verdaccio/login')
|
||||
.post('/-/verdaccio/sec/login')
|
||||
.send({
|
||||
username: credentials.name,
|
||||
password: credentials.password,
|
||||
@ -210,7 +210,7 @@ describe('endpoint web unit test', () => {
|
||||
|
||||
test('should fails on log unvalid user', (done) => {
|
||||
request(app)
|
||||
.post('/-/verdaccio/login')
|
||||
.post('/-/verdaccio/sec/login')
|
||||
.send({
|
||||
username: 'fake',
|
||||
password: 'fake',
|
||||
|
@ -17,13 +17,14 @@ import {
|
||||
Package,
|
||||
IPluginStorageFilter,
|
||||
Author,
|
||||
AuthPluginPackage,
|
||||
AuthPluginPackage,
|
||||
Token,
|
||||
ITokenActions,
|
||||
TokenFilter
|
||||
TokenFilter,
|
||||
RateLimit,
|
||||
} from '@verdaccio/types';
|
||||
import lunrMutable from 'lunr-mutable-indexes';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
export type StringValue = verdaccio$StringValue;
|
||||
|
||||
@ -31,6 +32,8 @@ export interface StartUpConfig {
|
||||
storage: string;
|
||||
plugins?: string;
|
||||
self_path: string;
|
||||
user_agent?: boolean;
|
||||
userRateLimit?: RateLimit;
|
||||
}
|
||||
|
||||
// legacy should be removed in long term
|
||||
@ -46,13 +49,13 @@ export type LegacyPackageAccess = PackageAccess & {
|
||||
proxy_access?: string[];
|
||||
// FIXME: should be published on @verdaccio/types
|
||||
unpublish?: string[];
|
||||
}
|
||||
};
|
||||
|
||||
export type MatchedPackage = PackageAccess | void;
|
||||
|
||||
export type JWTPayload = RemoteUser & {
|
||||
password?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface AESPayload {
|
||||
user: string;
|
||||
@ -96,10 +99,10 @@ export interface Profile {
|
||||
fullname: string;
|
||||
}
|
||||
|
||||
export type $RequestExtend = Request & {remote_user?: any; log: Logger}
|
||||
export type $ResponseExtend = Response & {cookies?: any}
|
||||
export type $RequestExtend = Request & { remote_user?: any; log: Logger };
|
||||
export type $ResponseExtend = Response & { cookies?: any };
|
||||
export type $NextFunctionVer = NextFunction & any;
|
||||
export type $SidebarPackage = Package & {latest: any}
|
||||
export type $SidebarPackage = Package & { latest: any };
|
||||
|
||||
export interface IAuthWebUI {
|
||||
jwtEncrypt(user: RemoteUser, signOptions: JWTSignOptions): Promise<string>;
|
||||
@ -198,4 +201,3 @@ export interface Styles {
|
||||
}
|
||||
|
||||
export type AuthorAvatar = Author & { avatar?: string };
|
||||
|
||||
|
BIN
yarn.lock
BIN
yarn.lock
Binary file not shown.
Loading…
Reference in New Issue
Block a user