http: better api key handling.

This commit is contained in:
Christopher Jeffrey 2016-08-13 01:23:25 -07:00
parent 2b630ad99c
commit 30b7421ea7
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 121 additions and 173 deletions

View File

@ -43,15 +43,6 @@ function HTTPClient(options) {
this.auth = options.auth;
this.rpc = new RPCClient(options);
if (this.apiKey) {
if (typeof this.apiKey === 'string') {
assert(utils.isHex(this.apiKey), 'API key must be a hex string.');
this.apiKey = new Buffer(this.apiKey, 'hex');
}
assert(Buffer.isBuffer(this.apiKey));
assert(this.apiKey.length === 32, 'API key must be 32 bytes.');
}
// Open automatically.
this.open();
}
@ -125,8 +116,7 @@ HTTPClient.prototype._open = function _open(callback) {
});
this.socket.on('connect', function() {
var apiKey = self.apiKey ? self.apiKey.toString('hex') : null;
self.socket.emit('auth', apiKey, function(err) {
self.socket.emit('auth', self.apiKey, function(err) {
if (err)
return callback(new Error(err.error));
callback();
@ -208,19 +198,15 @@ HTTPClient.prototype._request = function _request(method, endpoint, json, callba
json = null;
}
if (json && method === 'get') {
query = json;
json = true;
if (this.token) {
if (!json)
json = {};
json.token = this.token;
}
if (this.apiKey) {
if (method === 'get') {
query = query || {};
query.apiKey = this.apiKey.toString('hex');
} else {
json = json || {};
json.apiKey = this.apiKey.toString('hex');
}
if (json && method === 'get') {
query = json;
json = null;
}
request({
@ -228,7 +214,10 @@ HTTPClient.prototype._request = function _request(method, endpoint, json, callba
uri: this.uri + endpoint,
query: query,
json: json,
auth: this.auth,
auth: {
username: 'bitcoinrpc',
password: this.apiKey || ''
},
expect: 'json'
}, function(err, res, body) {
if (err)
@ -572,9 +561,7 @@ HTTPClient.prototype.getBlock = function getBlock(hash, callback) {
*/
HTTPClient.prototype.broadcast = function broadcast(tx, callback) {
var body = { tx: tx.toRaw().toString('hex') };
callback = utils.ensure(callback);
var body = { tx: toHex(tx) };
return this._post('/broadcast', body, callback);
};
@ -598,15 +585,11 @@ HTTPClient.prototype.walletSend = function walletSend(id, options, callback) {
options.outputs = options.outputs.map(function(output) {
return {
value: utils.btc(output.value),
address: output.address && output.address.toBase58
? output.address.toBase58()
: output.address,
script: output.script ? output.script.toRaw().toString('hex') : null
address: output.address,
script: toHex(output.script)
};
});
callback = utils.ensure(callback);
return this._post('/wallet/' + id + '/send', options, callback);
};
@ -619,8 +602,6 @@ HTTPClient.prototype.walletSend = function walletSend(id, options, callback) {
HTTPClient.prototype.walletRetoken = function walletRetoken(id, passphrase, callback) {
var options = { passphrase: passphrase };
callback = utils.ensure(callback);
return this._post('/wallet/' + id + '/retoken', options, function(err, body) {
if (err)
return callback(err);
@ -639,8 +620,6 @@ HTTPClient.prototype.walletRetoken = function walletRetoken(id, passphrase, call
HTTPClient.prototype.walletSetPassphrase = function walletSetPassphrase(id, old, new_, callback) {
var options = { old: old, passphrase: new_ };
callback = utils.ensure(callback);
return this._post('/wallet/' + id + '/passphrase', options, callback);
};
@ -653,7 +632,6 @@ HTTPClient.prototype.walletSetPassphrase = function walletSetPassphrase(id, old,
HTTPClient.prototype.walletCreate = function walletCreate(id, options, callback) {
options = utils.merge({}, options);
options.outputs = options.outputs || [];
if (options.rate)
options.rate = utils.btc(options.rate);
@ -661,15 +639,11 @@ HTTPClient.prototype.walletCreate = function walletCreate(id, options, callback)
options.outputs = options.outputs.map(function(output) {
return {
value: utils.btc(output.value),
address: output.address && output.address.toBase58
? output.address.toBase58()
: output.address,
script: output.script ? output.script.toRaw().toString('hex') : null
address: output.address,
script: toHex(output.script)
};
});
callback = utils.ensure(callback);
return this._post('/wallet/' + id + '/create', options, callback);
};
@ -686,14 +660,11 @@ HTTPClient.prototype.walletSign = function walletCreate(id, tx, options, callbac
if (typeof options === 'function') {
callback = options;
options = null;
options = {};
}
body = utils.merge({}, options || {}, {
tx: tx.toRaw().toString('hex')
});
callback = utils.ensure(callback);
body = utils.merge({}, options);
body.tx = toHex(tx);
return this._post('/wallet/' + id + '/sign', body, callback);
};
@ -705,10 +676,7 @@ HTTPClient.prototype.walletSign = function walletCreate(id, tx, options, callbac
*/
HTTPClient.prototype.walletFill = function walletFill(tx, callback) {
var body = { tx: tx.toRaw().toString('hex') };
callback = utils.ensure(callback);
var body = { tx: toHex(tx) };
return this._post('/wallet/_/fill', body, callback);
};
@ -735,8 +703,6 @@ HTTPClient.prototype.walletZap = function walletZap(id, account, age, callback)
assert(utils.isNumber(age));
callback = utils.ensure(callback);
return this._post('/wallet/' + id + '/zap', body, callback);
};
@ -761,8 +727,6 @@ HTTPClient.prototype.addKey = function addKey(id, account, key, callback) {
key = key.xpubkey || key;
options = { account: account, key: key };
callback = utils.ensure(callback);
return this._put('/wallet/' + id + '/key', options, callback);
};
@ -787,8 +751,6 @@ HTTPClient.prototype.removeKey = function removeKey(id, account, key, callback)
key = key.xpubkey || key;
options = { account: account, key: key };
callback = utils.ensure(callback);
return this._del('/wallet/' + id + '/key', options, callback);
};
@ -848,6 +810,23 @@ HTTPClient.prototype.getInfo = function getInfo(callback) {
return this._get('/', callback);
};
/*
* Helpers
*/
function toHex(obj) {
if (!obj)
return;
if (obj.toRaw)
obj = obj.toRaw();
if (Buffer.isBuffer(obj))
obj = obj.toString('hex');
return obj;
}
/*
* Expose
*/

View File

@ -35,23 +35,13 @@ function RPCClient(options) {
this.uri = options.uri || 'http://localhost:' + this.network.rpcPort;
this.apiKey = options.apiKey;
this.id = 0;
if (this.apiKey) {
if (typeof this.apiKey === 'string') {
assert(utils.isHex(this.apiKey), 'API key must be a hex string.');
this.apiKey = new Buffer(this.apiKey, 'hex');
}
assert(Buffer.isBuffer(this.apiKey));
assert(this.apiKey.length === 32, 'API key must be 32 bytes.');
}
}
/**
* Make an http request to endpoint.
* Make a json rpc request.
* @private
* @param {String} method
* @param {String} endpoint - Path.
* @param {Object} json - Body or query depending on method.
* @param {String} method - RPC method name.
* @param {Array} params - RPC parameters.
* @param {Function} callback - Returns [Error, Object?].
*/
@ -68,7 +58,7 @@ RPCClient.prototype.call = function call(method, params, callback) {
},
auth: {
username: 'bitcoinrpc',
password: this.apiKey ? this.apiKey.toString('hex') : ''
password: this.apiKey || ''
},
expect: 'json'
}, function(err, res, body) {

View File

@ -49,21 +49,21 @@ function HTTPServer(options) {
this.logger = options.logger || this.node.logger;
this.loaded = false;
this.apiKey = options.apiKey;
this.apiHash = null;
this.rpc = null;
if (this.apiKey) {
if (typeof this.apiKey === 'string') {
assert(utils.isHex(this.apiKey), 'API key must be a hex string.');
this.apiKey = new Buffer(this.apiKey, 'hex');
}
assert(Buffer.isBuffer(this.apiKey));
assert(this.apiKey.length === 32, 'API key must be 32 bytes.');
} else {
this.apiKey = bcoin.ec.random(32);
}
if (!this.apiKey)
this.apiKey = utils.toBase58(bcoin.ec.random(20));
if (options.noAuth)
assert(typeof this.apiKey === 'string', 'API key must be a string.');
assert(this.apiKey.length <= 200, 'API key must be under 200 bytes.');
this.apiHash = hash256(this.apiKey);
if (options.noAuth) {
this.apiKey = null;
this.apiHash = null;
}
options.sockets = true;
@ -124,7 +124,6 @@ HTTPServer.prototype._init = function _init() {
auth = new Buffer(parts[1], 'base64').toString('utf8');
parts = auth.split(':');
assert(parts.length >= 2, 'Invalid auth token.');
req.username = parts.shift();
req.password = parts.join(':');
@ -132,17 +131,37 @@ HTTPServer.prototype._init = function _init() {
next();
});
this.use(function(req, res, next, send) {
if (!self.apiHash)
return next();
if (utils.ccmp(hash256(req.password), self.apiHash))
return next();
res.setHeader('WWW-Authenticate', 'Basic realm="node"');
if (req.method === 'POST'
&& req.pathname === '/') {
send(401, {
result: null,
error: {
message: 'Bad auth.',
code: 1
},
id: req.body.id
});
return;
}
send(401, { error: 'Bad API key.' });
});
this.use(function(req, res, next, send) {
var params, options;
if (req.method === 'POST'
&& req.pathname === '/') {
req.options = {};
if (self.apiKey) {
assert(utils.isHex(req.password), 'API key must be a hex string.');
assert(req.password.length === 64, 'API key must be 32 bytes.');
req.password = new Buffer(req.password, 'hex');
}
return next();
}
@ -242,19 +261,10 @@ HTTPServer.prototype._init = function _init() {
if (params.passphrase)
options.passphrase = params.passphrase;
if (req.password) {
assert(utils.isHex(req.password), 'API key must be a hex string.');
assert(req.password.length === 64, 'API key must be 32 bytes.');
options.token = new Buffer(req.password, 'hex');
}
if (req.headers['x-bcoin-api-key'])
params.apiKey = req.headers['x-bcoin-api-key'];
if (params.apiKey) {
assert(utils.isHex(params.apiKey), 'API key must be a hex string.');
assert(params.apiKey.length === 64, 'API key must be 32 bytes.');
options.apiKey = new Buffer(params.apiKey, 'hex');
if (params.token) {
assert(utils.isHex(params.token), 'API key must be a hex string.');
assert(params.token.length === 64, 'API key must be 32 bytes.');
options.token = new Buffer(params.token, 'hex');
}
req.options = options;
@ -263,18 +273,6 @@ HTTPServer.prototype._init = function _init() {
});
this.use(function(req, res, next, send) {
if (req.method === 'POST'
&& req.pathname === '/') {
return next();
}
if (self.apiKey) {
if (!utils.ccmp(req.options.apiKey, self.apiKey)) {
send(403, { error: 'Forbidden.' });
return;
}
}
if (req.path.length < 2 || req.path[0] !== 'wallet')
return next();
@ -296,8 +294,7 @@ HTTPServer.prototype._init = function _init() {
if (err) {
self.logger.info('Auth failure for %s: %s.',
req.options.id, err.message);
res.setHeader('WWW-Authenticate', 'Basic realm="wallet"');
send(401, { error: err.message });
send(403, { error: err.message });
return;
}
@ -323,21 +320,6 @@ HTTPServer.prototype._init = function _init() {
if (!(req.body.method && req.body.params))
return next(new Error('Method not found.'));
if (self.apiKey) {
if (!utils.ccmp(req.password, self.apiKey)) {
res.setHeader('WWW-Authenticate', 'Basic realm="rpc"');
send(401, {
result: null,
error: {
message: 'Bad auth.',
code: 1
},
id: req.body.id
});
return;
}
}
if (!self.rpc) {
RPC = require('./rpc');
self.rpc = new RPC(self.node);
@ -548,10 +530,7 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
json = wallet.toJSON();
wallet.destroy();
send(200, json);
send(200, wallet.toJSON());
});
});
@ -561,9 +540,6 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
if (accounts.length === 0)
return send(404);
send(200, accounts);
});
});
@ -759,9 +735,6 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
if (!txs.length)
return send(404);
req.wallet.toDetails(txs, function(err, txs) {
if (err)
return next(err);
@ -780,9 +753,6 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
if (!txs.length)
return send(404);
req.wallet.toDetails(txs, function(err, txs) {
if (err)
return next(err);
@ -802,9 +772,6 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
if (!txs.length)
return send(404);
req.wallet.toDetails(txs, function(err, txs) {
if (err)
return next(err);
@ -816,17 +783,14 @@ HTTPServer.prototype._init = function _init() {
});
});
// Wallet TXs within time range
// Last Wallet TXs
this.get('/wallet/:id/tx/last', function(req, res, next, send) {
var account = req.options.account;
var limit = req.options.limit;
req.wallet.getRange(account, limit, function(err, txs) {
req.wallet.getLast(account, limit, function(err, txs) {
if (err)
return next(err);
if (!txs.length)
return send(404);
req.wallet.toDetails(txs, function(err, txs) {
if (err)
return next(err);
@ -885,19 +849,14 @@ HTTPServer.prototype._initIO = function _initIO() {
socket.on('auth', function(apiKey, callback) {
callback = utils.ensure(callback);
if (!self.apiKey) {
if (!self.apiHash) {
self.logger.info('Successful auth.');
socket.bcoin.stopTimeout();
self.emit('websocket', socket);
return callback();
}
if (!utils.isHex(apiKey))
return callback({ error: 'Bad key.' });
apiKey = new Buffer(apiKey, 'hex');
if (!utils.ccmp(apiKey, self.apiKey))
if (!utils.ccmp(hash256(apiKey), self.apiHash))
return callback({ error: 'Bad key.' });
self.logger.info('Successful auth.');
@ -990,10 +949,12 @@ HTTPServer.prototype._initIO = function _initIO() {
*/
HTTPServer.prototype.open = function open(callback) {
if (this.apiKey)
this.logger.info('API key: %s', this.apiKey.toString('hex'));
else
if (this.apiKey) {
this.logger.info('API key: %s', this.apiKey);
this.apiKey = null;
} else if (!this.apiHash) {
this.logger.warning('WARNING: Your http server is open to the world.');
}
this.server.open(callback);
};
@ -1103,6 +1064,18 @@ ClientSocket.prototype.destroy = function() {
this.socket.disconnect();
};
/*
* Helpers
*/
function hash256(data) {
if (typeof data !== 'string')
return new Buffer(0);
if (data.length > 200)
return new Buffer(0);
return utils.hash256(new Buffer(data, 'utf8'));
}
/*
* Expose
*/

View File

@ -91,14 +91,13 @@ HTTPWallet.prototype._init = function _init() {
HTTPWallet.prototype.open = function open(options, callback) {
var self = this;
if (Buffer.isBuffer(options.token))
options.token = options.token.toString('hex');
this.id = options.id;
if (options.token) {
this.client.auth = { username: 'x', password: options.token };
this.token = new Buffer(options.token, 'hex');
this.token = options.token;
if (Buffer.isBuffer(this.token))
this.token = this.token.toString('hex');
this.client.token = this.token;
}
this.client.open(function(err) {
@ -272,6 +271,14 @@ HTTPWallet.prototype.getAccounts = function getAccounts(callback) {
return this.client.getWalletAccounts(this.id, callback);
};
/**
* @see Wallet#getAccount
*/
HTTPWallet.prototype.getAccount = function getAccount(options, callback) {
return this.client.getWalletAccount(this.id, options, callback);
};
/**
* @see Wallet#createAccount
*/
@ -298,8 +305,8 @@ HTTPWallet.prototype.retoken = function retoken(passphrase, callback) {
if (err)
return callback(err);
self.client.auth = { username: 'x', password: token };
self.token = new Buffer(token, 'hex');
self.token = token;
self.client.token = token;
return callback(null, token);
});

View File

@ -29,20 +29,19 @@ var dummyInput = {
describe('HTTP', function() {
var request = bcoin.http.request;
var apiKey = utils.hash256(new Buffer([]));
var w, addr, hash;
this.timeout(15000);
var node = new bcoin.fullnode({
network: 'regtest',
apiKey: apiKey,
apiKey: 'foo',
walletAuth: true
});
var wallet = new bcoin.http.wallet({
network: 'regtest',
apiKey: apiKey
apiKey: 'foo'
});
node.on('error', function() {});