From a9b2728c076ecfa3454bfebf53a96d972c3e42c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Fri, 26 Jul 2019 15:59:24 +0200 Subject: [PATCH] add DCS interface, payload limits for DCS/OSC --- src/InputHandler.ts | 22 +++++++++++++------- src/Terminal.ts | 5 +++++ src/TestUtils.test.ts | 3 +++ src/Types.d.ts | 1 + src/common/parser/DcsParser.test.ts | 8 ++++---- src/common/parser/DcsParser.ts | 30 +++++++++++++++++++++++++--- src/common/parser/OscParser.ts | 21 ++++++++++++++++++- src/public/Terminal.ts | 3 +++ typings/xterm.d.ts | 31 +++++++++++++++++++++++++---- 9 files changed, 105 insertions(+), 19 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 04d433ab..62d95cdc 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -22,6 +22,7 @@ import { IAttributeData, IDisposable } from 'common/Types'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; import { OscHandlerFactory } from 'common/parser/OscParser'; +import { DcsHandlerFactory } from 'common/parser/DcsParser'; /** * Map collect to glevel. Used in `selectCharset`. @@ -485,13 +486,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowService.markDirty(buffer.y); } - /** - * Forward addEscHandler from parser. - */ - public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { - return this._parser.addEscHandler(collectAndFlag, handler); - } - /** * Forward addCsiHandler from parser. */ @@ -499,6 +493,20 @@ export class InputHandler extends Disposable implements IInputHandler { return this._parser.addCsiHandler(flag, callback); } + /** + * Forward addDcsHandler from parser. + */ + public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + return this._parser.addDcsHandler(collectAndFlag, new DcsHandlerFactory(callback)); + } + + /** + * Forward addEscHandler from parser. + */ + public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { + return this._parser.addEscHandler(collectAndFlag, handler); + } + /** * Forward addOscHandler from parser. */ diff --git a/src/Terminal.ts b/src/Terminal.ts index 0de1b3d5..bd5ddcc3 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1397,6 +1397,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this._inputHandler.addEscHandler(collectAndFlag, handler); } + /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ + public addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(collectAndFlag, callback); + } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { return this._inputHandler.addCsiHandler(flag, callback); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index efe41858..6552c3e3 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -77,6 +77,9 @@ export class MockTerminal implements ITerminal { addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable { throw new Error('Method not implemented.'); } + addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { throw new Error('Method not implemented.'); } diff --git a/src/Types.d.ts b/src/Types.d.ts index be488304..e6e540a3 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -201,6 +201,7 @@ export interface IPublicTerminal extends IDisposable { open(parent: HTMLElement): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable; + addDcsHandler(collectAndFlag: string, callback: (param: IParams, data: string) => boolean): IDisposable; addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable; addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts index d45d2770..a48b99dd 100644 --- a/src/common/parser/DcsParser.test.ts +++ b/src/common/parser/DcsParser.test.ts @@ -138,7 +138,7 @@ describe('DcsParser', () => { }); describe('DcsHandlerFactory', () => { it('should be called once on end(true)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params, data]))); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -148,7 +148,7 @@ describe('DcsParser', () => { assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]); }); it('should not be called on end(false)', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params, data]))); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push([params.toArray(), data]))); parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); @@ -158,8 +158,8 @@ describe('DcsParser', () => { assert.deepEqual(reports, []); }); it('should be disposable', () => { - parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params, data]))); - const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params, data]))); + parser.setDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['one', params.toArray(), data]))); + const dispo = parser.addDcsHandler('+p', new DcsHandlerFactory((params, data) => reports.push(['two', params.toArray(), data]))); parser.hook('+', Params.fromArray([1, 2, 3]), 'p'.charCodeAt(0)); let data = toUtf32('Here comes'); parser.put(data, 0, data.length); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index aef8c694..d3f777dd 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -6,6 +6,7 @@ import { IDisposable } from 'common/Types'; import { IDcsHandler, IParams, ParamsArray, IHandlerCollection, IDcsParser, DcsFallbackHandler } from 'common/parser/Types'; import { utf32ToString } from 'common/input/TextDecoder'; +import { Params } from './Params'; export class DcsParser implements IDcsParser { @@ -98,24 +99,47 @@ export class DcsParser implements IDcsParser { } } +// limit allowed payload for DcsHandlerFactory +const PAYLOAD_LIMIT = 50000000; + +/** + * Convenient class to create a DCS handler from a single callback function. + * Note: The payload is currently limited to 50 MB (hardcoded). + */ export class DcsHandlerFactory implements IDcsHandler { private _data = ''; private _params: IParams | undefined; - constructor(private _handler: (params: ParamsArray, data: string) => any) {} + private _hitLimit: boolean = false; + + constructor(private _handler: (params: IParams, data: string) => any) {} + public hook(collect: string, params: IParams, flag: number): void { this._params = params.clone(); this._data = ''; + this._hitLimit = false; } + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } } + public unhook(success: boolean): any { let ret; - if (success) { - ret = this._handler(this._params ? this._params.toArray() : [], this._data); + if (this._hitLimit) { + ret = false; + } else if (success) { + ret = this._handler(this._params ? this._params : new Params(), this._data); } this._params = undefined; this._data = ''; + this._hitLimit = false; return ret; } } diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index d6b83d64..f0c9c93c 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -163,25 +163,44 @@ export class OscParser extends Disposable { } +// limit allowed payload for OscHandlerFactory +const PAYLOAD_LIMIT = 50000000; + /** * Convenient class to allow attaching string based handler functions * as OSC handlers. */ export class OscHandlerFactory implements IOscHandler { private _data = ''; + private _hitLimit: boolean = false; + constructor(private _handler: (data: string) => any) {} + public start(): void { this._data = ''; + this._hitLimit = false; } + public put(data: Uint32Array, start: number, end: number): void { + if (this._hitLimit) { + return; + } this._data += utf32ToString(data, start, end); + if (this._data.length > PAYLOAD_LIMIT) { + this._data = ''; + this._hitLimit = true; + } } + public end(success: boolean): any { let ret; - if (success) { + if (this._hitLimit) { + ret = false; + } else if (success) { ret = this._handler(this._data); } this._data = ''; + this._hitLimit = false; return ret; } } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 391501e0..c9513f53 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -60,6 +60,9 @@ export class Terminal implements ITerminalApi { public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable { return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect)); } + public addDcsHandler(collectAndFlag: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable { + return this._core.addDcsHandler(collectAndFlag, (params: IParams, data: string) => callback(params.toArray(), data)); + } public addEscHandler(collectAndFlag: string, handler: () => boolean): IDisposable { return this._core.addEscHandler(collectAndFlag, handler); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7582f6d2..a0810309 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -515,10 +515,28 @@ declare module 'xterm' { */ addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable; + /** + * Adds a handler for DCS escape sequences. + * @param collect Should be a string, which specifies the collect and the + * final character (e.g "$q" for DECRQSS) of the DCS sequence. + * @param callback The function to handle the escape sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, those will be stored up + * until the sequence is finished. Since DCS sequences are not limited by the amount + * of data this might impose a problem for big payloads. Currently xterm.js limits + * DCS payload to 50 MB which should give enough room for most use cases. + * The function gets numerical parameter and the data as arguments. + * Return true if the sequence was handled; false if + * we should try a previous handler (set by addDcsHandler or setDcsHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addDcsHandler(collect: string, callback: (param: (number | number[])[], data: string) => boolean): IDisposable; + /** * Adds a handler for ESC escape sequences. - * @param flag The flag should be a string, which specifies the - * collect and the final character (e.g "%G" for default charset selection) + * @param collect Should be a string, which specifies the collect and the + * final character (e.g "%G" for default charset selection) * of the ESC sequence. * @param callback The function to handle the escape sequence. * Return true if the sequence was handled; false if @@ -531,8 +549,13 @@ declare module 'xterm' { /** * Adds a handler for OSC escape sequences. * @param ident The number (first parameter) of the sequence. - * @param callback The function to handle the escape sequence. The callback - * is called with OSC data string. Return true if the sequence was handled; + * @param callback The function to handle the escape sequence. Note that the + * function will only be called once if the sequence finished sucessfully. + * There is currently no way to intercept smaller data chunks, those will be stored up + * until the sequence is finished. Since OSC sequences are not limited by the amount + * of data this might impose a problem for big payloads. Currently xterm.js limits + * OSC payload to 50 MB which should give enough room for most use cases. + * The callback is called with OSC data string. Return true if the sequence was handled; * false if we should try a previous handler (set by addOscHandler or * setOscHandler). The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler.