mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-24 21:15:51 +01:00
feat: expose middleware utils (#3580)
* feat: expose middleware utils * feat: expose middleware utils * Update antiLoop.ts * Update e2e-ci.yml
This commit is contained in:
parent
6c3539ca28
commit
a1986e098d
8
.changeset/big-years-repair.md
Normal file
8
.changeset/big-years-repair.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
'@verdaccio/api': minor
|
||||||
|
'@verdaccio/middleware': minor
|
||||||
|
'@verdaccio/utils': minor
|
||||||
|
'@verdaccio/web': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: expose middleware utils
|
7
.github/workflows/e2e-ci.yml
vendored
7
.github/workflows/e2e-ci.yml
vendored
@ -75,14 +75,15 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pkg: [npm6, npm7, npm8, npm9, pnpm6, pnpm7, yarn1, yarn2, yarn3, yarn4]
|
pkg: [npm6, npm7, npm8, npm9, pnpm6, pnpm7, yarn1, yarn2, yarn3, yarn4]
|
||||||
name: ${{ matrix.pkg }} / ${{ matrix.os }}
|
node: [16, 18, 19]
|
||||||
|
name: ${{ matrix.pkg }}/ ubuntu-latest / ${{ matrix.node }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3
|
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # tag=v3
|
||||||
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # tag=v3
|
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # tag=v3
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version: ${{ matrix.node }}
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
run: npm i pnpm@6.32.15 -g
|
run: npm i pnpm@6.32.15 -g
|
||||||
- uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 # v3
|
- uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 # v3
|
||||||
|
@ -42,7 +42,6 @@ export default function (config: Config, auth: Auth, storage: Storage): Router {
|
|||||||
// TODO: For some reason? what reason?
|
// TODO: For some reason? what reason?
|
||||||
app.param('_rev', match(/^-rev$/));
|
app.param('_rev', match(/^-rev$/));
|
||||||
app.param('org_couchdb_user', match(/^org\.couchdb\.user:/));
|
app.param('org_couchdb_user', match(/^org\.couchdb\.user:/));
|
||||||
app.param('anything', match(/.*/));
|
|
||||||
app.use(auth.apiJWTmiddleware());
|
app.use(auth.apiJWTmiddleware());
|
||||||
app.use(bodyParser.json({ strict: false, limit: config.max_body_size || '10mb' }));
|
app.use(bodyParser.json({ strict: false, limit: config.max_body_size || '10mb' }));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../.babelrc"
|
"extends": "../../.babelrc",
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"node": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@babel/typescript"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
const config = require('../../jest/config');
|
const config = require('../../jest/config');
|
||||||
|
|
||||||
module.exports = Object.assign({}, config, {});
|
module.exports = Object.assign({}, config, {
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
lines: 67,
|
||||||
|
functions: 80,
|
||||||
|
branches: 56,
|
||||||
|
statements: 67,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -35,19 +35,23 @@
|
|||||||
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
|
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
|
||||||
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
|
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
|
||||||
"watch": "pnpm build:js -- --watch",
|
"watch": "pnpm build:js -- --watch",
|
||||||
|
"test": "jest",
|
||||||
"build": "pnpm run build:js && pnpm run build:types"
|
"build": "pnpm run build:js && pnpm run build:types"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "4.3.4",
|
|
||||||
"body-parser": "1.20.1",
|
|
||||||
"@verdaccio/auth": "workspace:6.0.0-6-next.34",
|
|
||||||
"@verdaccio/core": "workspace:6.0.0-6-next.55",
|
"@verdaccio/core": "workspace:6.0.0-6-next.55",
|
||||||
"@verdaccio/logger": "workspace:6.0.0-6-next.23",
|
"@verdaccio/logger": "workspace:6.0.0-6-next.23",
|
||||||
"@verdaccio/utils": "workspace:6.0.0-6-next.23",
|
"@verdaccio/utils": "workspace:6.0.0-6-next.23",
|
||||||
"lodash": "4.17.21"
|
"debug": "4.3.4",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"mime": "2.6.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/verdaccio"
|
"url": "https://opencollective.com/verdaccio"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"body-parser": "1.20.1",
|
||||||
|
"supertest": "6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,17 @@
|
|||||||
export * from './middleware';
|
export { match } from './middlewares/match';
|
||||||
|
export { setSecurityWebHeaders } from './middlewares/security-headers';
|
||||||
|
export { validateName, validatePackage } from './middlewares/validation';
|
||||||
|
export { media } from './middlewares/media';
|
||||||
|
export { encodeScopePackage } from './middlewares/encode-pkg';
|
||||||
|
export { expectJson } from './middlewares/json';
|
||||||
|
export { antiLoop } from './middlewares/antiLoop';
|
||||||
|
export { final } from './middlewares/final';
|
||||||
|
export { allow } from './middlewares/allow';
|
||||||
|
export { errorReportingMiddleware, handleError } from './middlewares/error';
|
||||||
|
export {
|
||||||
|
log,
|
||||||
|
LOG_STATUS_MESSAGE,
|
||||||
|
LOG_VERDACCIO_BYTES,
|
||||||
|
LOG_VERDACCIO_ERROR,
|
||||||
|
} from './middlewares/log';
|
||||||
|
export * from './types';
|
||||||
|
@ -1,413 +0,0 @@
|
|||||||
import buildDebug from 'debug';
|
|
||||||
import { NextFunction, Request, Response } from 'express';
|
|
||||||
import { HttpError } from 'http-errors';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import { Auth } from '@verdaccio/auth';
|
|
||||||
import {
|
|
||||||
API_ERROR,
|
|
||||||
HEADERS,
|
|
||||||
HEADER_TYPE,
|
|
||||||
HTTP_STATUS,
|
|
||||||
TOKEN_BASIC,
|
|
||||||
TOKEN_BEARER,
|
|
||||||
VerdaccioError,
|
|
||||||
errorUtils,
|
|
||||||
} from '@verdaccio/core';
|
|
||||||
import { logger } from '@verdaccio/logger';
|
|
||||||
import { Config, Logger, Package, RemoteUser } from '@verdaccio/types';
|
|
||||||
import {
|
|
||||||
isObject,
|
|
||||||
stringToMD5,
|
|
||||||
validateName as utilValidateName,
|
|
||||||
validatePackage as utilValidatePackage,
|
|
||||||
} from '@verdaccio/utils';
|
|
||||||
|
|
||||||
import { getVersionFromTarball } from './middleware-utils';
|
|
||||||
|
|
||||||
export type $RequestExtend = Request & { remote_user?: RemoteUser; log: Logger };
|
|
||||||
export type $ResponseExtend = Response & { cookies?: any };
|
|
||||||
export type $NextFunctionVer = NextFunction & any;
|
|
||||||
|
|
||||||
const debug = buildDebug('verdaccio:middleware');
|
|
||||||
|
|
||||||
export function match(regexp: RegExp): any {
|
|
||||||
return function (
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string
|
|
||||||
): void {
|
|
||||||
if (regexp.exec(value)) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
next('route');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: remove, was relocated to web package
|
|
||||||
// @ts-deprecated
|
|
||||||
export function setSecurityWebHeaders(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
// disable loading in frames (clickjacking, etc.)
|
|
||||||
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
|
||||||
// avoid stablish connections outside of domain
|
|
||||||
res.header(HEADERS.CSP, "connect-src 'self'");
|
|
||||||
// https://stackoverflow.com/questions/18337630/what-is-x-content-type-options-nosniff
|
|
||||||
res.header(HEADERS.CTO, 'nosniff');
|
|
||||||
// https://stackoverflow.com/questions/9090577/what-is-the-http-header-x-xss-protection
|
|
||||||
res.header(HEADERS.XSS, '1; mode=block');
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateName(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string,
|
|
||||||
name: string
|
|
||||||
): void {
|
|
||||||
if (value === '-') {
|
|
||||||
// special case in couchdb usually
|
|
||||||
next('route');
|
|
||||||
} else if (utilValidateName(value)) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
next(errorUtils.getForbidden('invalid ' + name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validatePackage(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string,
|
|
||||||
name: string
|
|
||||||
): void {
|
|
||||||
if (value === '-') {
|
|
||||||
// special case in couchdb usually
|
|
||||||
next('route');
|
|
||||||
} else if (utilValidatePackage(value)) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
next(errorUtils.getForbidden('invalid ' + name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function media(expect: string | null): any {
|
|
||||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
if (req.headers[HEADER_TYPE.CONTENT_TYPE] !== expect) {
|
|
||||||
next(
|
|
||||||
errorUtils.getCode(
|
|
||||||
HTTP_STATUS.UNSUPPORTED_MEDIA,
|
|
||||||
'wrong content-type, expect: ' + expect + ', got: ' + req.get[HEADER_TYPE.CONTENT_TYPE]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function encodeScopePackage(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
if (req.url.indexOf('@') !== -1) {
|
|
||||||
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
|
|
||||||
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expectJson(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
if (!isObject(req.body)) {
|
|
||||||
return next(errorUtils.getBadRequest("can't parse incoming json"));
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function antiLoop(config: Config): Function {
|
|
||||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
if (req?.headers?.via != null) {
|
|
||||||
const arr = req.headers.via.split(',');
|
|
||||||
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
const m = arr[i].match(/\s*(\S+)\s+(\S+)/);
|
|
||||||
if (m && m[2] === config.server_id) {
|
|
||||||
return next(errorUtils.getCode(HTTP_STATUS.LOOP_DETECTED, 'loop detected'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function allow(auth: Auth): Function {
|
|
||||||
return function (action: string): Function {
|
|
||||||
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
req.pause();
|
|
||||||
const packageName = req.params.scope
|
|
||||||
? `@${req.params.scope}/${req.params.package}`
|
|
||||||
: req.params.package;
|
|
||||||
const packageVersion = req.params.filename
|
|
||||||
? getVersionFromTarball(req.params.filename)
|
|
||||||
: undefined;
|
|
||||||
const remote = req.remote_user;
|
|
||||||
logger.trace(
|
|
||||||
{ action, user: remote?.name },
|
|
||||||
`[middleware/allow][@{action}] allow for @{user}`
|
|
||||||
);
|
|
||||||
auth['allow_' + action](
|
|
||||||
{ packageName, packageVersion },
|
|
||||||
remote,
|
|
||||||
function (error, allowed): void {
|
|
||||||
req.resume();
|
|
||||||
if (error) {
|
|
||||||
next(error);
|
|
||||||
} else if (allowed) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
// last plugin (that's our built-in one) returns either
|
|
||||||
// cb(err) or cb(null, true), so this should never happen
|
|
||||||
throw errorUtils.getInternalError(API_ERROR.PLUGIN_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MiddlewareError {
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FinalBody = Package | MiddlewareError | string;
|
|
||||||
|
|
||||||
export function final(
|
|
||||||
body: FinalBody,
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
// if we remove `next` breaks test
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader(HEADERS.WWW_AUTH)) {
|
|
||||||
// they say it's required for 401, so...
|
|
||||||
res.header(HEADERS.WWW_AUTH, `${TOKEN_BASIC}, ${TOKEN_BEARER}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (_.isString(body) || _.isObject(body)) {
|
|
||||||
if (!res.getHeader(HEADERS.CONTENT_TYPE)) {
|
|
||||||
res.header(HEADERS.CONTENT_TYPE, HEADERS.JSON);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof body === 'object' && _.isNil(body) === false) {
|
|
||||||
if (typeof (body as MiddlewareError).error === 'string') {
|
|
||||||
res.locals._verdaccio_error = (body as MiddlewareError).error;
|
|
||||||
// res._verdaccio_error = (body as MiddlewareError).error;
|
|
||||||
}
|
|
||||||
body = JSON.stringify(body, undefined, ' ') + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't send etags with errors
|
|
||||||
if (
|
|
||||||
!res.statusCode ||
|
|
||||||
(res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)
|
|
||||||
) {
|
|
||||||
res.header(HEADERS.ETAG, '"' + stringToMD5(body as string) + '"');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// send(null), send(204), etc.
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
// if verdaccio sends headers first, and then calls res.send()
|
|
||||||
// as an error handler, we can't report error properly,
|
|
||||||
// and should just close socket
|
|
||||||
if (err.message.match(/set headers after they are sent/)) {
|
|
||||||
if (_.isNil(res.socket) === false) {
|
|
||||||
res.socket?.destroy();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: deprecated, moved to @verdaccio/dev-commons
|
|
||||||
export const LOG_STATUS_MESSAGE =
|
|
||||||
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
|
||||||
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
|
|
||||||
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
|
|
||||||
|
|
||||||
export function log(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
|
||||||
// logger
|
|
||||||
req.log = logger.child({ sub: 'in' });
|
|
||||||
|
|
||||||
const _auth = req.headers.authorization;
|
|
||||||
if (_.isNil(_auth) === false) {
|
|
||||||
req.headers.authorization = '<Classified>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const _cookie = req.get('cookie');
|
|
||||||
if (_.isNil(_cookie) === false) {
|
|
||||||
req.headers.cookie = '<Classified>';
|
|
||||||
}
|
|
||||||
|
|
||||||
req.url = req.originalUrl;
|
|
||||||
req.log.info({ req: req, ip: req.ip }, "@{ip} requested '@{req.method} @{req.url}'");
|
|
||||||
req.originalUrl = req.url;
|
|
||||||
|
|
||||||
if (_.isNil(_auth) === false) {
|
|
||||||
req.headers.authorization = _auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isNil(_cookie) === false) {
|
|
||||||
req.headers.cookie = _cookie;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytesin = 0;
|
|
||||||
req.on('data', function (chunk): void {
|
|
||||||
bytesin += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
let bytesout = 0;
|
|
||||||
const _write = res.write;
|
|
||||||
// FIXME: res.write should return boolean
|
|
||||||
// @ts-ignore
|
|
||||||
res.write = function (buf): boolean {
|
|
||||||
bytesout += buf.length;
|
|
||||||
/* eslint prefer-rest-params: "off" */
|
|
||||||
// @ts-ignore
|
|
||||||
_write.apply(res, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const log = function (): void {
|
|
||||||
const forwardedFor = req.get('x-forwarded-for');
|
|
||||||
const remoteAddress = req.connection.remoteAddress;
|
|
||||||
const remoteIP = forwardedFor ? `${forwardedFor} via ${remoteAddress}` : remoteAddress;
|
|
||||||
let message;
|
|
||||||
if (res.locals._verdaccio_error) {
|
|
||||||
message = LOG_VERDACCIO_ERROR;
|
|
||||||
} else {
|
|
||||||
message = LOG_VERDACCIO_BYTES;
|
|
||||||
}
|
|
||||||
|
|
||||||
req.url = req.originalUrl;
|
|
||||||
req.log.http(
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
},
|
|
||||||
user: (req.remote_user && req.remote_user.name) || null,
|
|
||||||
remoteIP,
|
|
||||||
status: res.statusCode,
|
|
||||||
error: res.locals._verdaccio_error,
|
|
||||||
bytes: {
|
|
||||||
in: bytesin,
|
|
||||||
out: bytesout,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
message
|
|
||||||
);
|
|
||||||
req.originalUrl = req.url;
|
|
||||||
};
|
|
||||||
|
|
||||||
req.on('close', function (): void {
|
|
||||||
log();
|
|
||||||
});
|
|
||||||
|
|
||||||
const _end = res.end;
|
|
||||||
// @ts-ignore
|
|
||||||
res.end = function (buf): void {
|
|
||||||
if (buf) {
|
|
||||||
bytesout += buf.length;
|
|
||||||
}
|
|
||||||
/* eslint prefer-rest-params: "off" */
|
|
||||||
// @ts-ignore
|
|
||||||
_end.apply(res, arguments);
|
|
||||||
log();
|
|
||||||
};
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleError(
|
|
||||||
err: HttpError,
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
) {
|
|
||||||
debug('error handler init');
|
|
||||||
if (_.isError(err)) {
|
|
||||||
debug('is native error');
|
|
||||||
if (err.code === 'ECONNABORT' && res.statusCode === HTTP_STATUS.NOT_MODIFIED) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
if (_.isFunction(res.locals.report_error) === false) {
|
|
||||||
debug('is locals error report ref');
|
|
||||||
// in case of very early error this middleware may not be loaded before error is generated
|
|
||||||
// fixing that
|
|
||||||
errorReportingMiddleware(req, res, _.noop);
|
|
||||||
}
|
|
||||||
debug('set locals error report ref');
|
|
||||||
res.locals.report_error(err);
|
|
||||||
} else {
|
|
||||||
// Fall to Middleware.final
|
|
||||||
debug('no error to report, jump next layer');
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
export function errorReportingMiddleware(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
debug('error report middleware');
|
|
||||||
res.locals.report_error =
|
|
||||||
res.locals.report_error ||
|
|
||||||
function (err: VerdaccioError): void {
|
|
||||||
if (err.status && err.status >= HTTP_STATUS.BAD_REQUEST && err.status < 600) {
|
|
||||||
debug('is error > 409 %o', err?.status);
|
|
||||||
if (_.isNil(res.headersSent) === false) {
|
|
||||||
debug('send status %o', err?.status);
|
|
||||||
res.status(err.status);
|
|
||||||
debug('next layer %o', err?.message);
|
|
||||||
next({ error: err.message || API_ERROR.UNKNOWN_ERROR });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug('is error < 409 %o', err?.status);
|
|
||||||
logger.error({ err: err }, 'unexpected error: @{!err.message}\n@{err.stack}');
|
|
||||||
if (!res.status || !res.send) {
|
|
||||||
// TODO: decide which debug keep
|
|
||||||
logger.error('this is an error in express.js, please report this');
|
|
||||||
debug('this is an error in express.js, please report this, destroy response %o', err);
|
|
||||||
res.destroy();
|
|
||||||
} else if (!res.headersSent) {
|
|
||||||
debug('report internal error %o', err);
|
|
||||||
res.status(HTTP_STATUS.INTERNAL_ERROR);
|
|
||||||
next({ error: API_ERROR.INTERNAL_SERVER_ERROR });
|
|
||||||
} else {
|
|
||||||
// socket should be already closed
|
|
||||||
debug('this should not happen, otherwise report %o', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
debug('error report middleware next()');
|
|
||||||
next();
|
|
||||||
}
|
|
40
packages/middleware/src/middlewares/allow.ts
Normal file
40
packages/middleware/src/middlewares/allow.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { API_ERROR, errorUtils } from '@verdaccio/core';
|
||||||
|
import { logger } from '@verdaccio/logger';
|
||||||
|
import { getVersionFromTarball } from '@verdaccio/utils';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
export function allow<T>(auth: T): Function {
|
||||||
|
return function (action: string): Function {
|
||||||
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
|
req.pause();
|
||||||
|
const packageName = req.params.scope
|
||||||
|
? `@${req.params.scope}/${req.params.package}`
|
||||||
|
: req.params.package;
|
||||||
|
const packageVersion = req.params.filename
|
||||||
|
? getVersionFromTarball(req.params.filename)
|
||||||
|
: undefined;
|
||||||
|
const remote = req.remote_user;
|
||||||
|
logger.trace(
|
||||||
|
{ action, user: remote?.name },
|
||||||
|
`[middleware/allow][@{action}] allow for @{user}`
|
||||||
|
);
|
||||||
|
auth['allow_' + action](
|
||||||
|
{ packageName, packageVersion },
|
||||||
|
remote,
|
||||||
|
function (error, allowed): void {
|
||||||
|
req.resume();
|
||||||
|
if (error) {
|
||||||
|
next(error);
|
||||||
|
} else if (allowed) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
// last plugin (that's our built-in one) returns either
|
||||||
|
// cb(err) or cb(null, true), so this should never happen
|
||||||
|
throw errorUtils.getInternalError(API_ERROR.PLUGIN_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
30
packages/middleware/src/middlewares/antiLoop.ts
Normal file
30
packages/middleware/src/middlewares/antiLoop.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { HTTP_STATUS, errorUtils } from '@verdaccio/core';
|
||||||
|
import { Config } from '@verdaccio/types';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A middleware that avoid a registry points itself as proxy and avoid create infinite loops.
|
||||||
|
* @param config
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function antiLoop(config: Config): Function {
|
||||||
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
|
if (req?.headers?.via != null) {
|
||||||
|
const arr = req.get('via')?.split(',');
|
||||||
|
if (Array.isArray(arr)) {
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
// the "via" header must contains an specific headers, this has to be on sync
|
||||||
|
// with the proxy request
|
||||||
|
// match eg: Server 1 or Server 2
|
||||||
|
// TODO: improve this RegEX
|
||||||
|
const m = arr[i].trim().match(/\s*(\S+)\s+(\S+)/);
|
||||||
|
if (m && m[2] === config.server_id) {
|
||||||
|
return next(errorUtils.getCode(HTTP_STATUS.LOOP_DETECTED, 'loop detected'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
19
packages/middleware/src/middlewares/encode-pkg.ts
Normal file
19
packages/middleware/src/middlewares/encode-pkg.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode / in a scoped package name to be matched as a single parameter in routes
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
export function encodeScopePackage(
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer
|
||||||
|
): void {
|
||||||
|
if (req.url.indexOf('@') !== -1) {
|
||||||
|
// e.g.: /@org/pkg/1.2.3 -> /@org%2Fpkg/1.2.3, /@org%2Fpkg/1.2.3 -> /@org%2Fpkg/1.2.3
|
||||||
|
req.url = req.url.replace(/^(\/@[^\/%]+)\/(?!$)/, '$1%2F');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
78
packages/middleware/src/middlewares/error.ts
Normal file
78
packages/middleware/src/middlewares/error.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import buildDebug from 'debug';
|
||||||
|
import { HttpError } from 'http-errors';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { API_ERROR, HTTP_STATUS, VerdaccioError } from '@verdaccio/core';
|
||||||
|
import { logger } from '@verdaccio/logger';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio:middleware:error');
|
||||||
|
|
||||||
|
export function handleError(
|
||||||
|
err: HttpError,
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer
|
||||||
|
) {
|
||||||
|
debug('error handler init');
|
||||||
|
if (_.isError(err)) {
|
||||||
|
debug('is native error');
|
||||||
|
if (err.code === 'ECONNABORT' && res.statusCode === HTTP_STATUS.NOT_MODIFIED) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
if (_.isFunction(res.locals.report_error) === false) {
|
||||||
|
debug('is locals error report ref');
|
||||||
|
// in case of very early error this middleware may not be loaded before error is generated
|
||||||
|
// fixing that
|
||||||
|
errorReportingMiddleware(req, res, _.noop);
|
||||||
|
}
|
||||||
|
debug('set locals error report ref');
|
||||||
|
res.locals.report_error(err);
|
||||||
|
} else {
|
||||||
|
// Fall to Middleware.final
|
||||||
|
debug('no error to report, jump next layer');
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
export function errorReportingMiddleware(
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer
|
||||||
|
): void {
|
||||||
|
debug('error report middleware');
|
||||||
|
res.locals.report_error =
|
||||||
|
res.locals.report_error ||
|
||||||
|
function (err: VerdaccioError): void {
|
||||||
|
if (err.status && err.status >= HTTP_STATUS.BAD_REQUEST && err.status < 600) {
|
||||||
|
debug('is error > 409 %o', err?.status);
|
||||||
|
if (_.isNil(res.headersSent) === false) {
|
||||||
|
debug('send status %o', err?.status);
|
||||||
|
res.status(err.status);
|
||||||
|
debug('next layer %o', err?.message);
|
||||||
|
next({ error: err.message || API_ERROR.UNKNOWN_ERROR });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug('is error < 409 %o', err?.status);
|
||||||
|
logger.error({ err: err }, 'unexpected error: @{!err.message}\n@{err.stack}');
|
||||||
|
if (!res.status || !res.send) {
|
||||||
|
// TODO: decide which debug keep
|
||||||
|
logger.error('this is an error in express.js, please report this');
|
||||||
|
debug('this is an error in express.js, please report this, destroy response %o', err);
|
||||||
|
res.destroy();
|
||||||
|
} else if (!res.headersSent) {
|
||||||
|
debug('report internal error %o', err);
|
||||||
|
res.status(HTTP_STATUS.INTERNAL_ERROR);
|
||||||
|
next({ error: API_ERROR.INTERNAL_SERVER_ERROR });
|
||||||
|
} else {
|
||||||
|
// socket should be already closed
|
||||||
|
debug('this should not happen, otherwise report %o', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
debug('error report middleware next()');
|
||||||
|
next();
|
||||||
|
}
|
60
packages/middleware/src/middlewares/final.ts
Normal file
60
packages/middleware/src/middlewares/final.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { HEADERS, HTTP_STATUS, TOKEN_BASIC, TOKEN_BEARER } from '@verdaccio/core';
|
||||||
|
import { Manifest } from '@verdaccio/types';
|
||||||
|
import { stringToMD5 } from '@verdaccio/utils';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend, MiddlewareError } from '../types';
|
||||||
|
|
||||||
|
export type FinalBody = Manifest | MiddlewareError | string;
|
||||||
|
|
||||||
|
export function final(
|
||||||
|
body: FinalBody,
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
// if we remove `next` breaks test
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
next: $NextFunctionVer
|
||||||
|
): void {
|
||||||
|
if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader(HEADERS.WWW_AUTH)) {
|
||||||
|
res.header(HEADERS.WWW_AUTH, `${TOKEN_BASIC}, ${TOKEN_BEARER}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (_.isString(body) || _.isObject(body)) {
|
||||||
|
if (!res.get(HEADERS.CONTENT_TYPE)) {
|
||||||
|
res.header(HEADERS.CONTENT_TYPE, HEADERS.JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof body === 'object' && _.isNil(body) === false) {
|
||||||
|
if (typeof (body as MiddlewareError).error === 'string') {
|
||||||
|
res.locals._verdaccio_error = (body as MiddlewareError).error;
|
||||||
|
}
|
||||||
|
body = JSON.stringify(body, undefined, ' ') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't send etags with errors
|
||||||
|
if (
|
||||||
|
!res.statusCode ||
|
||||||
|
(res.statusCode >= HTTP_STATUS.OK && res.statusCode < HTTP_STATUS.MULTIPLE_CHOICES)
|
||||||
|
) {
|
||||||
|
res.header(HEADERS.ETAG, '"' + stringToMD5(body as string) + '"');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// send(null), send(204), etc.
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// if verdaccio sends headers first, and then calls res.send()
|
||||||
|
// as an error handler, we can't report error properly,
|
||||||
|
// and should just close socket
|
||||||
|
if (err.message.match(/set headers after they are sent/)) {
|
||||||
|
if (_.isNil(res.socket) === false) {
|
||||||
|
res.socket?.destroy();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(body);
|
||||||
|
}
|
15
packages/middleware/src/middlewares/json.ts
Normal file
15
packages/middleware/src/middlewares/json.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { errorUtils } from '@verdaccio/core';
|
||||||
|
import { isObject } from '@verdaccio/utils';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
export function expectJson(
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer
|
||||||
|
): void {
|
||||||
|
if (!isObject(req.body)) {
|
||||||
|
return next(errorUtils.getBadRequest("can't parse incoming json"));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
103
packages/middleware/src/middlewares/log.ts
Normal file
103
packages/middleware/src/middlewares/log.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { logger } from '@verdaccio/logger';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
// FIXME: deprecated, moved to @verdaccio/dev-commons
|
||||||
|
export const LOG_STATUS_MESSAGE =
|
||||||
|
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
|
||||||
|
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
|
||||||
|
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
|
||||||
|
|
||||||
|
export function log(req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
|
// logger
|
||||||
|
req.log = logger.child({ sub: 'in' });
|
||||||
|
|
||||||
|
const _auth = req.headers.authorization;
|
||||||
|
if (_.isNil(_auth) === false) {
|
||||||
|
req.headers.authorization = '<Classified>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const _cookie = req.get('cookie');
|
||||||
|
if (_.isNil(_cookie) === false) {
|
||||||
|
req.headers.cookie = '<Classified>';
|
||||||
|
}
|
||||||
|
|
||||||
|
req.url = req.originalUrl;
|
||||||
|
req.log.info({ req: req, ip: req.ip }, "@{ip} requested '@{req.method} @{req.url}'");
|
||||||
|
req.originalUrl = req.url;
|
||||||
|
|
||||||
|
if (_.isNil(_auth) === false) {
|
||||||
|
req.headers.authorization = _auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isNil(_cookie) === false) {
|
||||||
|
req.headers.cookie = _cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytesin = 0;
|
||||||
|
req.on('data', function (chunk): void {
|
||||||
|
bytesin += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
let bytesout = 0;
|
||||||
|
const _write = res.write;
|
||||||
|
// FIXME: res.write should return boolean
|
||||||
|
// @ts-ignore
|
||||||
|
res.write = function (buf): boolean {
|
||||||
|
bytesout += buf.length;
|
||||||
|
/* eslint prefer-rest-params: "off" */
|
||||||
|
// @ts-ignore
|
||||||
|
_write.apply(res, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = function (): void {
|
||||||
|
const forwardedFor = req.get('x-forwarded-for');
|
||||||
|
const remoteAddress = req.connection.remoteAddress;
|
||||||
|
const remoteIP = forwardedFor ? `${forwardedFor} via ${remoteAddress}` : remoteAddress;
|
||||||
|
let message;
|
||||||
|
if (res.locals._verdaccio_error) {
|
||||||
|
message = LOG_VERDACCIO_ERROR;
|
||||||
|
} else {
|
||||||
|
message = LOG_VERDACCIO_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.url = req.originalUrl;
|
||||||
|
req.log.http(
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
},
|
||||||
|
user: req.remote_user?.name || null,
|
||||||
|
remoteIP,
|
||||||
|
status: res.statusCode,
|
||||||
|
error: res.locals._verdaccio_error,
|
||||||
|
bytes: {
|
||||||
|
in: bytesin,
|
||||||
|
out: bytesout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message
|
||||||
|
);
|
||||||
|
req.originalUrl = req.url;
|
||||||
|
};
|
||||||
|
|
||||||
|
req.on('close', function (): void {
|
||||||
|
log();
|
||||||
|
});
|
||||||
|
|
||||||
|
const _end = res.end;
|
||||||
|
// @ts-ignore
|
||||||
|
res.end = function (buf): void {
|
||||||
|
if (buf) {
|
||||||
|
bytesout += buf.length;
|
||||||
|
}
|
||||||
|
/* eslint prefer-rest-params: "off" */
|
||||||
|
// @ts-ignore
|
||||||
|
_end.apply(res, arguments);
|
||||||
|
log();
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
}
|
16
packages/middleware/src/middlewares/match.ts
Normal file
16
packages/middleware/src/middlewares/match.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
export function match(regexp: RegExp): any {
|
||||||
|
return function (
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer,
|
||||||
|
value: string
|
||||||
|
): void {
|
||||||
|
if (regexp.exec(value)) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next('route');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
18
packages/middleware/src/middlewares/media.ts
Normal file
18
packages/middleware/src/middlewares/media.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { HEADER_TYPE, HTTP_STATUS, errorUtils } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
export function media(expect: string | null): any {
|
||||||
|
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
|
if (req.headers[HEADER_TYPE.CONTENT_TYPE] !== expect) {
|
||||||
|
next(
|
||||||
|
errorUtils.getCode(
|
||||||
|
HTTP_STATUS.UNSUPPORTED_MEDIA,
|
||||||
|
'wrong content-type, expect: ' + expect + ', got: ' + req.get[HEADER_TYPE.CONTENT_TYPE]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
21
packages/middleware/src/middlewares/security-headers.ts
Normal file
21
packages/middleware/src/middlewares/security-headers.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { HEADERS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
// TODO: remove, was relocated to web package
|
||||||
|
// @ts-deprecated
|
||||||
|
export function setSecurityWebHeaders(
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer
|
||||||
|
): void {
|
||||||
|
// disable loading in frames (clickjacking, etc.)
|
||||||
|
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
||||||
|
// avoid stablish connections outside of domain
|
||||||
|
res.header(HEADERS.CSP, "connect-src 'self'");
|
||||||
|
// https://stackoverflow.com/questions/18337630/what-is-x-content-type-options-nosniff
|
||||||
|
res.header(HEADERS.CTO, 'nosniff');
|
||||||
|
// https://stackoverflow.com/questions/9090577/what-is-the-http-header-x-xss-protection
|
||||||
|
res.header(HEADERS.XSS, '1; mode=block');
|
||||||
|
next();
|
||||||
|
}
|
41
packages/middleware/src/middlewares/validation.ts
Normal file
41
packages/middleware/src/middlewares/validation.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { errorUtils } from '@verdaccio/core';
|
||||||
|
import {
|
||||||
|
validateName as utilValidateName,
|
||||||
|
validatePackage as utilValidatePackage,
|
||||||
|
} from '@verdaccio/utils';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
export function validateName(
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer,
|
||||||
|
value: string,
|
||||||
|
name: string
|
||||||
|
): void {
|
||||||
|
if (value === '-') {
|
||||||
|
// special case in couchdb usually
|
||||||
|
next('route');
|
||||||
|
} else if (utilValidateName(value)) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(errorUtils.getForbidden('invalid ' + name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validatePackage(
|
||||||
|
req: $RequestExtend,
|
||||||
|
res: $ResponseExtend,
|
||||||
|
next: $NextFunctionVer,
|
||||||
|
value: string,
|
||||||
|
name: string
|
||||||
|
): void {
|
||||||
|
if (value === '-') {
|
||||||
|
// special case in couchdb usually
|
||||||
|
next('route');
|
||||||
|
} else if (utilValidatePackage(value)) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
next(errorUtils.getForbidden('invalid ' + name));
|
||||||
|
}
|
||||||
|
}
|
11
packages/middleware/src/types.ts
Normal file
11
packages/middleware/src/types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { Logger, RemoteUser } from '@verdaccio/types';
|
||||||
|
|
||||||
|
export type $RequestExtend = Request & { remote_user?: RemoteUser; log: Logger };
|
||||||
|
export type $ResponseExtend = Response & { cookies?: any };
|
||||||
|
export type $NextFunctionVer = NextFunction & any;
|
||||||
|
|
||||||
|
export interface MiddlewareError {
|
||||||
|
error: string;
|
||||||
|
}
|
82
packages/middleware/test/allow.spec.ts
Normal file
82
packages/middleware/test/allow.spec.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
import { setup } from '@verdaccio/logger';
|
||||||
|
|
||||||
|
import { allow } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
setup({});
|
||||||
|
|
||||||
|
test('should allow request', async () => {
|
||||||
|
const can = allow({
|
||||||
|
allow_publish: (params, remove, cb) => {
|
||||||
|
return cb(null, true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/:package', can('publish'), (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/react').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow scope request', async () => {
|
||||||
|
const can = allow({
|
||||||
|
allow_publish: (params, remove, cb) => {
|
||||||
|
return cb(null, true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/:package/:scope', can('publish'), (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/@verdaccio/core').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow filename request', async () => {
|
||||||
|
const can = allow({
|
||||||
|
allow_publish: (params, remove, cb) => {
|
||||||
|
return cb(null, true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/:filename', can('publish'), (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/aaa-0.0.1.tgz').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not allow request', async () => {
|
||||||
|
const can = allow({
|
||||||
|
allow_publish: (params, remove, cb) => {
|
||||||
|
return cb(null, false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/sec', can('publish'), (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/sec').expect(HTTP_STATUS.INTERNAL_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle error request', async () => {
|
||||||
|
const can = allow({
|
||||||
|
allow_publish: (params, remove, cb) => {
|
||||||
|
return cb(Error('foo error'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/err', can('publish'));
|
||||||
|
|
||||||
|
return request(app).get('/err').expect(HTTP_STATUS.INTERNAL_ERROR);
|
||||||
|
});
|
21
packages/middleware/test/encode.spec.ts
Normal file
21
packages/middleware/test/encode.spec.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { encodeScopePackage } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
test('encode is json', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(encodeScopePackage);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
res.status(HTTP_STATUS.OK).json({ id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app).get('/@scope/foo');
|
||||||
|
expect(res.body).toEqual({ id: '@scope/foo' });
|
||||||
|
expect(res.status).toEqual(HTTP_STATUS.OK);
|
||||||
|
});
|
60
packages/middleware/test/final.spec.ts
Normal file
60
packages/middleware/test/final.spec.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import express from 'express';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HEADERS, HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { final } from '../src';
|
||||||
|
|
||||||
|
test('handle error as object', async () => {
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json({ strict: false, limit: '10mb' }));
|
||||||
|
app.get('/401', (req, res, next) => {
|
||||||
|
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
next({ error: 'some error' });
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(final);
|
||||||
|
|
||||||
|
const res = await request(app).get('/401');
|
||||||
|
expect(res.get(HEADERS.WWW_AUTH)).toEqual('Basic, Bearer');
|
||||||
|
expect(res.get(HEADERS.CONTENT_TYPE)).toEqual(HEADERS.JSON_CHARSET);
|
||||||
|
expect(res.get(HEADERS.ETAG)).toEqual('W/"1c-CP1UoQiM59AjHpEk0334sfSp1kc"');
|
||||||
|
expect(res.body).toEqual({ error: 'some error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle error as string', async () => {
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json({ strict: false, limit: '10mb' }));
|
||||||
|
app.get('/200', (req, res, next) => {
|
||||||
|
res.status(HTTP_STATUS.OK);
|
||||||
|
// error as json string
|
||||||
|
next(JSON.stringify({ error: 'some error' }));
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(final);
|
||||||
|
|
||||||
|
const res = await request(app).get('/200');
|
||||||
|
expect(res.get(HEADERS.WWW_AUTH)).not.toBeDefined();
|
||||||
|
expect(res.get(HEADERS.CONTENT_TYPE)).toEqual(HEADERS.JSON_CHARSET);
|
||||||
|
expect(res.get(HEADERS.ETAG)).toEqual('"3f3a7b9afa23269e16685af6e707d109"');
|
||||||
|
expect(res.body).toEqual({ error: 'some error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle error as unknown string no parsable', async () => {
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json({ strict: false, limit: '10mb' }));
|
||||||
|
app.get('/200', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK);
|
||||||
|
// error as json string
|
||||||
|
throw Error('uknonwn');
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(final);
|
||||||
|
|
||||||
|
const res = await request(app).get('/200');
|
||||||
|
expect(res.get(HEADERS.WWW_AUTH)).not.toBeDefined();
|
||||||
|
expect(res.get(HEADERS.CONTENT_TYPE)).toEqual(HEADERS.JSON_CHARSET);
|
||||||
|
expect(res.get(HEADERS.ETAG)).toEqual('"8a80554c91d9fca8acb82f023de02f11"');
|
||||||
|
expect(res.body).toEqual({});
|
||||||
|
});
|
14
packages/middleware/test/helper.ts
Normal file
14
packages/middleware/test/helper.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
export const getApp = (middlewares = []) => {
|
||||||
|
const app = express();
|
||||||
|
middlewares.map((middleware) => {
|
||||||
|
app.use(middleware);
|
||||||
|
});
|
||||||
|
|
||||||
|
// app.get('/', function (req, res) {
|
||||||
|
// res.status(200).json({ name: 'pkg' });
|
||||||
|
// });
|
||||||
|
|
||||||
|
return app;
|
||||||
|
};
|
32
packages/middleware/test/json.spec.ts
Normal file
32
packages/middleware/test/json.spec.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HEADERS, HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { expectJson } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
test('body is json', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
app.use(bodyParser.json({ strict: false, limit: '10mb' }));
|
||||||
|
// @ts-ignore
|
||||||
|
app.put('/json', expectJson, (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app)
|
||||||
|
.put('/json')
|
||||||
|
.send({ name: 'john' })
|
||||||
|
.set(HEADERS.CONTENT_TYPE, 'application/json')
|
||||||
|
.expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('body is not json', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.put('/json', expectJson, (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).put('/json').send('test=4').expect(HTTP_STATUS.BAD_REQUEST);
|
||||||
|
});
|
28
packages/middleware/test/log.spec.ts
Normal file
28
packages/middleware/test/log.spec.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
import { setup } from '@verdaccio/logger';
|
||||||
|
|
||||||
|
import { log } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
setup({
|
||||||
|
type: 'file',
|
||||||
|
path: path.join(__dirname, './verdaccio.log'),
|
||||||
|
level: 'trace',
|
||||||
|
format: 'json',
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should log request', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(log);
|
||||||
|
// @ts-ignore
|
||||||
|
app.get('/:package', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: pending output
|
||||||
|
return request(app).get('/react').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
31
packages/middleware/test/loop.spec.ts
Normal file
31
packages/middleware/test/loop.spec.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { antiLoop } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
test('should not be a loop', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(antiLoop({ server_id: '1' }));
|
||||||
|
app.get('/sec', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/sec').set('via', 'Server 2').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be a loop', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(antiLoop({ server_id: '1' }));
|
||||||
|
app.get('/sec', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app)
|
||||||
|
.get('/sec')
|
||||||
|
.set('via', 'Server 1, Server 2')
|
||||||
|
.expect(HTTP_STATUS.LOOP_DETECTED);
|
||||||
|
});
|
33
packages/middleware/test/media.spec.ts
Normal file
33
packages/middleware/test/media.spec.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import mime from 'mime';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HEADERS, HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { media } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
test('media is json', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
app.get('/json', media(mime.getType('json')), (req, res) => {
|
||||||
|
res.status(200).json();
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app)
|
||||||
|
.get('/json')
|
||||||
|
.set(HEADERS.CONTENT_TYPE, 'application/json')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('media is not json', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
app.get('/json', media(mime.getType('json')), (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app)
|
||||||
|
.get('/json')
|
||||||
|
.set(HEADERS.CONTENT_TYPE, 'text/html; charset=utf-8')
|
||||||
|
.expect('Content-Type', /html/)
|
||||||
|
.expect(HTTP_STATUS.UNSUPPORTED_MEDIA);
|
||||||
|
});
|
83
packages/middleware/test/params.spec.ts
Normal file
83
packages/middleware/test/params.spec.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { match, validateName, validatePackage } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
describe('validate params', () => {
|
||||||
|
test('should validate package name', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.param('package', validatePackage);
|
||||||
|
app.get('/pkg/:package', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/pkg/react').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fails validate package name', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.param('package', validatePackage);
|
||||||
|
app.get('/pkg/:package', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/pkg/node_modules').expect(HTTP_STATUS.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fails file name package name', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.param('filename', validateName);
|
||||||
|
app.get('/file/:filename', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/file/__proto__').expect(HTTP_STATUS.FORBIDDEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate file name package name', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.param('filename', validateName);
|
||||||
|
app.get('/file/:filename', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/file/react.tar.gz').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('match', () => {
|
||||||
|
test('should not match middleware', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
app.param('_rev', match(/^-rev$/));
|
||||||
|
app.param('org_couchdb_user', match(/^org\.couchdb\.user:/));
|
||||||
|
app.get('/-/user/:org_couchdb_user', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((res: any) => {
|
||||||
|
res.status(HTTP_STATUS.INTERNAL_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/-/user/test').expect(HTTP_STATUS.INTERNAL_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match middleware', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
app.param('_rev', match(/^-rev$/));
|
||||||
|
app.get('/-/user/:_rev?', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((res: any) => {
|
||||||
|
res.status(HTTP_STATUS.INTERNAL_ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(app).get('/-/user/-rev').expect(HTTP_STATUS.OK);
|
||||||
|
});
|
||||||
|
});
|
54
packages/middleware/test/security.spec.ts
Normal file
54
packages/middleware/test/security.spec.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
import { HEADERS, HTTP_STATUS } from '@verdaccio/core';
|
||||||
|
|
||||||
|
import { setSecurityWebHeaders } from '../src';
|
||||||
|
import { getApp } from './helper';
|
||||||
|
|
||||||
|
test('should get frame options', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(setSecurityWebHeaders);
|
||||||
|
app.get('/sec', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app).get('/sec').expect(HTTP_STATUS.OK);
|
||||||
|
expect(res.get(HEADERS.FRAMES_OPTIONS)).toEqual('deny');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get csp options', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(setSecurityWebHeaders);
|
||||||
|
app.get('/sec', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app).get('/sec').expect(HTTP_STATUS.OK);
|
||||||
|
expect(res.get(HEADERS.CSP)).toEqual("connect-src 'self'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get cto', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(setSecurityWebHeaders);
|
||||||
|
app.get('/sec', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app).get('/sec').expect(HTTP_STATUS.OK);
|
||||||
|
expect(res.get(HEADERS.CTO)).toEqual('nosniff');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get xss', async () => {
|
||||||
|
const app = getApp([]);
|
||||||
|
// @ts-ignore
|
||||||
|
app.use(setSecurityWebHeaders);
|
||||||
|
app.get('/sec', (req, res) => {
|
||||||
|
res.status(HTTP_STATUS.OK).json({});
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request(app).get('/sec').expect(HTTP_STATUS.OK);
|
||||||
|
expect(res.get(HEADERS.XSS)).toEqual('1; mode=block');
|
||||||
|
});
|
@ -3,3 +3,4 @@ export * from './utils';
|
|||||||
export * from './crypto-utils';
|
export * from './crypto-utils';
|
||||||
export * from './replace-lodash';
|
export * from './replace-lodash';
|
||||||
export * from './matcher';
|
export * from './matcher';
|
||||||
|
export * from './middleware-utils';
|
||||||
|
@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
|
||||||
import { Auth } from '@verdaccio/auth';
|
import { Auth } from '@verdaccio/auth';
|
||||||
import { match, validateName, validatePackage } from '@verdaccio/middleware';
|
import { validateName, validatePackage } from '@verdaccio/middleware';
|
||||||
import { Storage } from '@verdaccio/store';
|
import { Storage } from '@verdaccio/store';
|
||||||
import { Config } from '@verdaccio/types';
|
import { Config } from '@verdaccio/types';
|
||||||
|
|
||||||
@ -17,7 +17,6 @@ export function webAPI(config: Config, auth: Auth, storage: Storage): Router {
|
|||||||
route.param('package', validatePackage);
|
route.param('package', validatePackage);
|
||||||
route.param('filename', validateName);
|
route.param('filename', validateName);
|
||||||
route.param('version', validateName);
|
route.param('version', validateName);
|
||||||
route.param('anything', match(/.*/));
|
|
||||||
route.use(bodyParser.urlencoded({ extended: false }));
|
route.use(bodyParser.urlencoded({ extended: false }));
|
||||||
route.use(auth.webUIJWTmiddleware());
|
route.use(auth.webUIJWTmiddleware());
|
||||||
route.use(setSecurityWebHeaders);
|
route.use(setSecurityWebHeaders);
|
||||||
|
512
pnpm-lock.yaml
generated
512
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user