Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

fix(product): Prevent Prototype pollution #766

Merged
merged 3 commits into from
Sep 27, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/product/ProductAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ describe('ProductAPI', () => {
});
});

describe('getCandleWatcherConfig', () => {
it('throws an error when supplying an invalid product ID', () => {
const test = (): void => {
global.client.rest.product.getCandleWatcherConfig('invalid-product-id', CandleGranularity.ONE_DAY);
};
expect(test).toThrowError();
});
});

describe('getCandles', () => {
it('returns the latest candles when not giving any parameters', async () => {
nock(global.REST_URL)
Expand Down Expand Up @@ -370,6 +379,13 @@ describe('ProductAPI', () => {
});

describe('unwatchCandles', () => {
it('does not remove an unregistered interval', () => {
const test = (): void => {
global.client.rest.product.unwatchCandles('invalid-product-id', CandleGranularity.ONE_DAY);
};
expect(test).not.toThrowError();
});

it('removes running candle watching intervals', async () => {
const productId = 'BTC-USD';

Expand Down
52 changes: 33 additions & 19 deletions src/product/ProductAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ export interface Candle {

type RawCandle = [Timestamp, Low, High, Open, Close, Volume];

type CandleWatcherConfig = {
expectedISO: ISO_8601_MS_UTC;
intervalId: NodeJS.Timeout;
};

export enum ProductEvent {
NEW_CANDLE = 'ProductEvent.NEW_CANDLE',
}
Expand All @@ -179,14 +184,7 @@ export class ProductAPI {
PRODUCTS: `/products`,
};

private watchCandlesConfig: {
[productId: string]: {
[granularity: number]: {
expectedISO: ISO_8601_MS_UTC;
intervalId: NodeJS.Timeout;
};
};
} = {};
private watchCandlesConfig: Map<string, CandleWatcherConfig> = new Map();

constructor(private readonly apiClient: AxiosInstance, private readonly restClient: RESTClient) {}

Expand Down Expand Up @@ -238,6 +236,19 @@ export class ProductAPI {
.sort((a, b) => a.openTimeInMillis - b.openTimeInMillis);
}

private composeCandleWatcherKey(productId: string, granularity: CandleGranularity): string {
return `${productId}@${granularity}`;
}

getCandleWatcherConfig(productId: string, granularity: CandleGranularity): CandleWatcherConfig {
const key = this.composeCandleWatcherKey(productId, granularity);
const config = this.watchCandlesConfig.get(key);
if (config) {
return config;
}
throw new Error(`There is no candle watching config with key "${key}".`);
}

/**
* Watch a specific product ID for new candles. Candles will be emitted through the `ProductEvent.NEW_CANDLE` event.
*
Expand All @@ -247,19 +258,20 @@ export class ProductAPI {
* @returns Handle to stop the watch interval
*/
watchCandles(productId: string, granularity: CandleGranularity, lastCandleTime: ISO_8601_MS_UTC): void {
this.watchCandlesConfig[productId] = this.watchCandlesConfig[productId] || {};
if (this.watchCandlesConfig[productId][granularity]) {
const key = this.composeCandleWatcherKey(productId, granularity);

if (this.watchCandlesConfig.get(key)) {
throw new Error(
`You are already watching "${productId}" with an interval of "${granularity}" seconds. Please clear this interval before creating a new one.`
);
} else {
const expectedISO = CandleBucketUtil.addUnitISO(lastCandleTime, granularity, 1);
const intervalId = this.startCandleInterval(productId, granularity);

this.watchCandlesConfig[productId][granularity] = {
this.watchCandlesConfig.set(key, {
expectedISO,
intervalId,
};
});
}
}

Expand All @@ -270,11 +282,11 @@ export class ProductAPI {
* @param granularity - Desired candle size
*/
unwatchCandles(productId: string, granularity: CandleGranularity): void {
const intervalId = this.watchCandlesConfig[productId][granularity].intervalId;
clearInterval(intervalId);
delete this.watchCandlesConfig[productId][granularity];
if (Object.values(this.watchCandlesConfig[productId]).length === 0) {
delete this.watchCandlesConfig[productId];
const key = this.composeCandleWatcherKey(productId, granularity);
const config = this.watchCandlesConfig.get(key);
if (config) {
clearInterval(config.intervalId);
this.watchCandlesConfig.delete(key);
}
}

Expand Down Expand Up @@ -404,15 +416,17 @@ export class ProductAPI {
}

private emitCandle(productId: string, granularity: CandleGranularity, candle: Candle): void {
const config = this.getCandleWatcherConfig(productId, granularity);
// Emit matched candle
this.restClient.emit(ProductEvent.NEW_CANDLE, productId, granularity, candle);
// Cache timestamp of upcoming candle
const nextOpenTime = CandleBucketUtil.addUnitISO(candle.openTimeInMillis, granularity, 1);
this.watchCandlesConfig[productId][granularity].expectedISO = nextOpenTime;
config.expectedISO = nextOpenTime;
}

private async checkNewCandles(productId: string, granularity: CandleGranularity): Promise<void> {
const expectedTimestampISO = this.watchCandlesConfig[productId][granularity].expectedISO;
const config = this.getCandleWatcherConfig(productId, granularity);
const expectedTimestampISO = config.expectedISO;

const candles = await this.getCandles(productId, {
granularity,
Expand Down