Time synchronization with server

- wrapper for /v2/public/time rest endpoint
- synchronize time before trying to authenticate a request in order
  to avoid invalid authentication because prevented replay attacks.

Resolves #1
This commit is contained in:
Stefan Aebischer
2019-09-16 14:17:48 +02:00
parent ed28d14171
commit b6d2803a49
4 changed files with 47 additions and 6 deletions

View File

@@ -64,6 +64,14 @@ If you only use the [public endpoints](#public-endpoints) you can ommit key and
#### async getLatestInformation() #### async getLatestInformation()
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#latest-information-for-symbol) [See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#latest-information-for-symbol)
#### async getServerTime()
[See bybit documentation](https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/rest_api.md#server-time)
#### async getTimeOffset()
Returns the time offset in ms to the server time retrieved by [`async getServerTime`](#async-getservertime).
If positive the time on the server is ahead of the clients time, if negative the time on the server is behind the clients time.
## Example ## Example

View File

@@ -3,7 +3,7 @@ const assert = require('assert');
const request = require('request'); const request = require('request');
const {signMessage} = require('./utility.js'); const {signMessage, getServerTimeOffset} = require('./utility.js');
const baseUrls = { const baseUrls = {
livenet: 'https://api.bybit.com', livenet: 'https://api.bybit.com',
@@ -14,9 +14,13 @@ module.exports = class Request {
constructor(key, secret, livenet=false) { constructor(key, secret, livenet=false) {
this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet']; this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet'];
this._timeOffset = null;
if(key) assert(secret, 'Secret is required for private enpoints'); if(key) assert(secret, 'Secret is required for private enpoints');
this._syncTime();
setInterval(this._syncTime.bind(this), 3600000);
this.key = key; this.key = key;
this.secret = secret; this.secret = secret;
} }
@@ -34,12 +38,22 @@ module.exports = class Request {
return result; return result;
} }
async getTimeOffset() {
const start = Date.now();
const result = await this.get('/v2/public/time');
const end = Date.now();
return Math.ceil((result.time_now * 1000) - start + ((end - start) / 2));
}
async _call(method, endpoint, params) { async _call(method, endpoint, params) {
const publicEndpoint = endpoint.startsWith('/v2/public'); const publicEndpoint = endpoint.startsWith('/v2/public');
if(!publicEndpoint) { if(!publicEndpoint) {
if(!this.key || !this.secret) throw new Error('Private endpoints require api and private keys set'); if(!this.key || !this.secret) throw new Error('Private endpoints require api and private keys set');
if(this._timeOffset === null) await this._syncTime();
params = this._signRequest(params); params = this._signRequest(params);
} }
@@ -73,7 +87,7 @@ module.exports = class Request {
const params = { const params = {
...data, ...data,
api_key: this.key, api_key: this.key,
timestamp: Date.now() timestamp: Date.now() + this._timeOffset
}; };
if(this.key && this.secret) { if(this.key && this.secret) {
@@ -89,4 +103,8 @@ module.exports = class Request {
.map(key => `${key}=${params[key]}`) .map(key => `${key}=${params[key]}`)
.join('&'); .join('&');
} }
async _syncTime() {
this._timeOffset = await this.getTimeOffset();
}
} }

View File

@@ -121,4 +121,12 @@ module.exports = class RestClient {
async getLatestInformation() { async getLatestInformation() {
return await this.request.get('/v2/public/tickers'); return await this.request.get('/v2/public/tickers');
} }
async getServerTime() {
return await this.request.get('/v2/public/time');
}
async getTimeOffset() {
return await this.request.getTimeOffset();
}
} }

View File

@@ -3,6 +3,7 @@ const {EventEmitter} = require('events');
const WebSocket = require('ws'); const WebSocket = require('ws');
const defaultLogger = require('./logger.js'); const defaultLogger = require('./logger.js');
const RestClient = require('./rest-client.js');
const {signMessage} = require('./utility.js'); const {signMessage} = require('./utility.js');
const wsUrls = { const wsUrls = {
@@ -33,6 +34,8 @@ module.exports = class WebsocketClient extends EventEmitter {
...options ...options
} }
this.client = new RestClient(null, null, this.options.livenet);
this._connect(); this._connect();
} }
@@ -54,10 +57,11 @@ module.exports = class WebsocketClient extends EventEmitter {
this.ws.close(); this.ws.close();
} }
_connect() { async _connect() {
if(this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING; if(this.readyState === READY_STATE_INITIAL) this.readyState = READY_STATE_CONNECTING;
const url = wsUrls[this.options.livenet ? 'livenet' : 'testnet'] + this._authenticate(); const authParams = await this._authenticate();
const url = wsUrls[this.options.livenet ? 'livenet' : 'testnet'] + authParams;
this.ws = new WebSocket(url); this.ws = new WebSocket(url);
@@ -67,12 +71,15 @@ module.exports = class WebsocketClient extends EventEmitter {
this.ws.on('close', this._wsCloseHandler.bind(this)); this.ws.on('close', this._wsCloseHandler.bind(this));
} }
_authenticate() { async _authenticate() {
if(this.options.key && this.options.secret) { if(this.options.key && this.options.secret) {
this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'}); this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'});
const timeOffset = await this.client.getTimeOffset();
const params = { const params = {
api_key: this.options.key, api_key: this.options.key,
expires: (Date.now() + 10000) expires: (Date.now() + timeOffset + 5000)
}; };
params.signature = signMessage('GET/realtime' + params.expires, this.options.secret); params.signature = signMessage('GET/realtime' + params.expires, this.options.secret);