Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const crossorigin = require('cors');
const helmet = require('helmet');
const Config = require('./config');
const Storage = require('storj-service-storage-models');
const middleware = require('storj-service-middleware');
const { querystring } = require('./server/middleware/query-string');
const { ErrorHandlerFactory: errorHandler } = require('./server/middleware/error-handler');

const Server = require('./server');
const pow = require('./server/middleware/pow');
const Mailer = require('inxt-service-mailer');
Expand All @@ -26,7 +28,7 @@ const bullQueueModule = require('./core/queue/bullQueue');

const QUEUE_NAME = 'NETWORK_WORKER_TASKS_QUEUE';

const getAllowedOrigins = ()=> {
const getAllowedOrigins = () => {
const originsEnv = process.env.ALLOWED_ORIGINS;

if (!originsEnv) {
Expand Down Expand Up @@ -139,7 +141,7 @@ Engine.prototype.start = function (callback) {
});
this.redis.on('error', (err) => {
log.error('error connecting to redis', err);
});
});

this.server = new Server(this._config.server, this._configureApp());

Expand Down Expand Up @@ -247,14 +249,14 @@ Engine.prototype._configureApp = function () {
self._keepPendingResponsesClean();

app.use(requestLogger);
app.use(middleware.querystring);
app.use(querystring);
app.use(this._trackResponseStatus.bind(this));
app.use(crossorigin(corsOptions));
app.use(helmet());
app.get('/', this._handleRootGET.bind(this));
routers.forEach(bindRoute);
app.use(unexpectedErrorLogger);
app.use(middleware.errorhandler({ logger: log }));
app.use(errorHandler({ logger: log }));

app.use(json());

Expand Down
31 changes: 31 additions & 0 deletions lib/server/error-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @module inxt-bridge/server/errors
*/

export class HTTPError extends Error {
code: number;
statusCode: number;

constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
this.code = statusCode;
Object.setPrototypeOf(this, new.target.prototype);
}
}

function makeError(statusCode: number, defaultMessage: string) {
return (message?: string) => new HTTPError(statusCode, message ?? defaultMessage);
}

export const RateLimited = makeError(429, 'Request rate limited');
export const NotFoundError = makeError(404, 'Resource not found');
export const NotAuthorizedError = makeError(401, 'Not authorized');
export const ForbiddenError = makeError(403, 'Forbidden');
export const InternalError = makeError(500, 'Internal error');
export const BadRequestError = makeError(400, 'Bad request');
export const NotImplementedError = makeError(501, 'Not implemented');
export const ServiceUnavailableError = makeError(503, 'Service Unavailable');
export const TransferRateError = makeError(420, 'Transfer rate limit');
export const ConflictError = makeError(409, 'Conflict');
export const UnprocessableEntityError = makeError(422, 'Unprocessable entity');
8 changes: 4 additions & 4 deletions lib/server/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ import { MongoDBBucketEntryShardsRepository } from "../../core/bucketEntryShards
import { Notifications } from "../notifications";
import { FileStateRepository } from "../../core/fileState/Repository";
import { MongoDBFileStateRepository } from "../../core/fileState/MongoDBFileStateRepository";

const { authenticate } = require('storj-service-middleware');
import authenticateFactory from "../middleware/authenticate";

interface Models {
User: any;
Expand All @@ -47,6 +46,8 @@ interface Models {
Shard: any;
BucketEntryShard: any;
FileState: any;
PublicKey: any;
UserNonce: any;
}

export function bindNewRoutes(
Expand Down Expand Up @@ -109,8 +110,7 @@ export function bindNewRoutes(
usersRepository,
fileStateRepository
);

const basicAuthMiddleware = authenticate(storage);
const basicAuthMiddleware = authenticateFactory({ User: storage.models.User, PublicKey: storage.models.PublicKey, UserNonce: storage.models.UserNonce });
const secretToUtf8 = Buffer.from(getEnv().gateway.jwtSecret, 'base64').toString('utf8')
const jwtMiddleware = buildJwtMiddleware(secretToUtf8, { algorithms: ['RS256'] });

Expand Down
2 changes: 1 addition & 1 deletion lib/server/http/middleware/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RequestHandler } from "express";
import { verify, VerifyOptions } from 'jsonwebtoken';
import { BadRequestError, ForbiddenError, NotAuthorizedError } from "storj-service-error-types";
import { BadRequestError, ForbiddenError, NotAuthorizedError } from '../../error-types';

export function buildMiddleware(secret: string, opts: Partial<VerifyOptions>): RequestHandler {
return (req, _, next) => {
Expand Down
2 changes: 1 addition & 1 deletion lib/server/http/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HTTPUsersController } from './controller';

export const createUsersHTTPRouter = (
controller: HTTPUsersController,
basicAuth: RequestHandler
basicAuth: RequestHandler | RequestHandler[]
): Router => {
const router = Router();

Expand Down
2 changes: 1 addition & 1 deletion lib/server/limiter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const log = require('../logger');
const errors = require('storj-service-error-types');
const errors = require('./error-types');
const tmpl = '{"rate_limited": {"url": "%s", "method": "%s", "ip": "%s"}}';

module.exports.DEFAULTS = (total = 1000, expire = 1000 * 60) => {
Expand Down
198 changes: 198 additions & 0 deletions lib/server/middleware/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* @module inxt-bridge/server/middleware/authenticate
*/

'use strict';

const secp256k1 = require('secp256k1');
const errors = require('../error-types');
const url = require('url');
const crypto = require('crypto');
const rawBodyMiddleware = require('./raw-body');
const basicauth = require('basic-auth');
const fromBody = ['POST', 'PATCH', 'PUT'];
const fromQuery = ['GET', 'DELETE', 'OPTIONS'];

function AuthenticateMiddlewareFactory({ User, PublicKey, UserNonce }) {
const models = { User, PublicKey, UserNonce };

function authenticate(req, res, next) {
let strategy = AuthenticateMiddlewareFactory._detectStrategy(req);

switch (strategy) {
case 'BASIC':
AuthenticateMiddlewareFactory._basic(models, req, res, next);
break;
case 'ECDSA':
AuthenticateMiddlewareFactory._ecdsa(models, req, res, next);
break;
default:
next(errors.NotAuthorizedError(
'No authentication strategy detected'
));
}
}

return [rawBodyMiddleware, authenticate];
}

AuthenticateMiddlewareFactory._basic = function ({ User }, req, res, next) {
let creds = basicauth(req);
User.lookup(creds.name, creds.pass, function (err, user) {
if (err) {
return next(err);
}

if (!user.activated) {
return next(errors.ForbiddenError(
'User account has not been activated'
));
}

req.user = user;

next();
});
};

AuthenticateMiddlewareFactory._ecdsa = async function ({ User, PublicKey, UserNonce }, req, res, next) {
if (!AuthenticateMiddlewareFactory._verifySignature(req)) {
return next(errors.NotAuthorizedError('Invalid signature'));
}

try {
const pubkey = await PublicKey.findOne({
_id: req.header('x-pubkey')
});

if (!pubkey) {
return next(errors.NotAuthorizedError(
'Public key not registered'
));
}

let params = AuthenticateMiddlewareFactory._getParams(req);

const user = await User.findOne({ _id: pubkey.user });

if (!user) {
return next(errors.NotAuthorizedError('User not found'));
}

if (!user.activated) {
return next(errors.ForbiddenError(
'User account has not been activated'
));
}

var userNonce = new UserNonce({
user: user.id,
nonce: params.__nonce
});

try {
await userNonce.save();
req.user = user;
req.pubkey = pubkey;

return next();
} catch (err) {
if (err.code === 11000) {
return next(errors.NotAuthorizedError(
'Invalid nonce supplied'
));
}

return next(err);
}
} catch (err) {
return next(err);
}

};

/**
* Returns a string representation of the auth type detected
* @private
* @param {http.IncomingMessage} req
* @returns {String}
*/
AuthenticateMiddlewareFactory._detectStrategy = function (req) {
const creds = basicauth(req);
const basic = creds && creds.name && creds.pass;
const ecdsa = req.header('x-signature') && req.header('x-pubkey');

return basic ? 'BASIC' : (ecdsa ? 'ECDSA' : 'NONE');
};

/**
* Extracts the payload for signature verification
* @private
* @param {http.IncomingMessage} req
* @returns {String}
*/
AuthenticateMiddlewareFactory._getPayload = function (req) {
if (fromBody.indexOf(req.method) !== -1) {
return req.rawbody;
}

if (fromQuery.indexOf(req.method) !== -1) {
return url.parse(req.url).query;
}

return '';
};

/**
* Extracts the request parameters
* @private
* @param {http.IncomingMessage} req
* @returns {Object}
*/
AuthenticateMiddlewareFactory._getParams = function (req) {
if (fromBody.indexOf(req.method) !== -1) {
return req.body;
}

if (fromQuery.indexOf(req.method) !== -1) {
return req.query;
}

return {};
};

AuthenticateMiddlewareFactory._getHash = function (req) {
let contract = Buffer.from([
req.method,
req.path,
AuthenticateMiddlewareFactory._getPayload(req)
].join('\n'), 'utf8');

return crypto.createHash('sha256').update(contract).digest('hex')
};

AuthenticateMiddlewareFactory._verifySignature = function (req) {
let signature;
let sigHeader = req.header('x-signature');
let pubkey = req.header('x-pubkey');
let signatureBuf;
let pubkeyBuf;

try {
pubkeyBuf = Buffer.from(pubkey, 'hex');
signatureBuf = Buffer.from(sigHeader, 'hex');
signature = secp256k1.signatureImport(signatureBuf);
} catch (e) {
return false;
}

let hash = AuthenticateMiddlewareFactory._getHash(req);

return secp256k1.ecdsaVerify(
secp256k1.signatureNormalize(signature),
Buffer.from(hash, 'hex'),
pubkeyBuf
);
};

module.exports = AuthenticateMiddlewareFactory;
28 changes: 28 additions & 0 deletions lib/server/middleware/error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @module inxt-bridge/server/middleware/error-handler
*/

import { ErrorRequestHandler } from 'express';

interface Logger {
error: (message: string, ...args: unknown[]) => void;
}

function ErrorHandlerFactory(options: { logger?: Logger }): ErrorRequestHandler {
const log: Logger = options.logger ?? console;

return function errorhandler(err, req, res, next) {
if (err) {
const statusCode = err.code ? (err.code > 500 ? 400 : err.code) : 500;
if (statusCode >= 500) {
log.error('request error: %s', err.message);
log.error(err.stack);
}

return res.status(statusCode).send({ error: err.message });
}
next();
};
}

export { ErrorHandlerFactory };
Loading
Loading