1
0
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:
Juan Picado 2021-12-24 19:04:15 +01:00 committed by GitHub
parent f64e403f0a
commit 5b1264c733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 183 additions and 170 deletions

@ -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

@ -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"],

Binary file not shown.

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

@ -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;
}

@ -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

Binary file not shown.