From b6d2803a494d7630b09eb2fce622281dcc883b69 Mon Sep 17 00:00:00 2001 From: Stefan Aebischer Date: Mon, 16 Sep 2019 14:17:48 +0200 Subject: [PATCH] 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 --- doc/rest-client.md | 8 ++++++++ lib/request.js | 22 ++++++++++++++++++++-- lib/rest-client.js | 8 ++++++++ lib/websocket-client.js | 15 +++++++++++---- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/doc/rest-client.md b/doc/rest-client.md index bcc8a2d..728280f 100644 --- a/doc/rest-client.md +++ b/doc/rest-client.md @@ -64,6 +64,14 @@ If you only use the [public endpoints](#public-endpoints) you can ommit key and #### async getLatestInformation() [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 diff --git a/lib/request.js b/lib/request.js index a211cd1..e609a20 100644 --- a/lib/request.js +++ b/lib/request.js @@ -3,7 +3,7 @@ const assert = require('assert'); const request = require('request'); -const {signMessage} = require('./utility.js'); +const {signMessage, getServerTimeOffset} = require('./utility.js'); const baseUrls = { livenet: 'https://api.bybit.com', @@ -14,9 +14,13 @@ module.exports = class Request { constructor(key, secret, livenet=false) { this.baseUrl = baseUrls[livenet === true ? 'livenet' : 'testnet']; + this._timeOffset = null; if(key) assert(secret, 'Secret is required for private enpoints'); + this._syncTime(); + setInterval(this._syncTime.bind(this), 3600000); + this.key = key; this.secret = secret; } @@ -34,12 +38,22 @@ module.exports = class Request { 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) { const publicEndpoint = endpoint.startsWith('/v2/public'); if(!publicEndpoint) { 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); } @@ -73,7 +87,7 @@ module.exports = class Request { const params = { ...data, api_key: this.key, - timestamp: Date.now() + timestamp: Date.now() + this._timeOffset }; if(this.key && this.secret) { @@ -89,4 +103,8 @@ module.exports = class Request { .map(key => `${key}=${params[key]}`) .join('&'); } + + async _syncTime() { + this._timeOffset = await this.getTimeOffset(); + } } diff --git a/lib/rest-client.js b/lib/rest-client.js index 7904762..242287c 100644 --- a/lib/rest-client.js +++ b/lib/rest-client.js @@ -121,4 +121,12 @@ module.exports = class RestClient { async getLatestInformation() { 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(); + } } diff --git a/lib/websocket-client.js b/lib/websocket-client.js index 181f19e..4c91783 100644 --- a/lib/websocket-client.js +++ b/lib/websocket-client.js @@ -3,6 +3,7 @@ const {EventEmitter} = require('events'); const WebSocket = require('ws'); const defaultLogger = require('./logger.js'); +const RestClient = require('./rest-client.js'); const {signMessage} = require('./utility.js'); const wsUrls = { @@ -33,6 +34,8 @@ module.exports = class WebsocketClient extends EventEmitter { ...options } + this.client = new RestClient(null, null, this.options.livenet); + this._connect(); } @@ -54,10 +57,11 @@ module.exports = class WebsocketClient extends EventEmitter { this.ws.close(); } - _connect() { + async _connect() { 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); @@ -67,12 +71,15 @@ module.exports = class WebsocketClient extends EventEmitter { this.ws.on('close', this._wsCloseHandler.bind(this)); } - _authenticate() { + async _authenticate() { if(this.options.key && this.options.secret) { this.logger.debug('Starting authenticated websocket client.', {category: 'bybit-ws'}); + + const timeOffset = await this.client.getTimeOffset(); + const params = { api_key: this.options.key, - expires: (Date.now() + 10000) + expires: (Date.now() + timeOffset + 5000) }; params.signature = signMessage('GET/realtime' + params.expires, this.options.secret);