From 243f2cc00e0d775b80c227f82fb53913a81db2fc Mon Sep 17 00:00:00 2001 From: Alexander Mac Date: Tue, 18 Mar 2025 21:21:26 +0300 Subject: [PATCH 1/2] fix Content-Disposition parameters encoding --- src/builders/base.ts | 12 ++++----- src/constants.ts | 36 ++++++++++++++------------- src/parsers/base.ts | 12 ++++----- src/parsers/form-data-param-parser.ts | 6 ++--- test/parsers/request.spec.ts | 14 +++++------ 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/builders/base.ts b/src/builders/base.ts index fb8f264..50cf331 100644 --- a/src/builders/base.ts +++ b/src/builders/base.ts @@ -1,5 +1,5 @@ import { assertArray, assertNotEmptyString, assertString } from '../assertions' -import { EOL, HttpContentApplicationType, HttpContentMultipartType, HttpHeader } from '../constants' +import { EOL, HttpContentTypeApplication, HttpContentTypeMultipart, HttpHeader } from '../constants' import { HttpZBody, HttpZHeader, HttpZParam } from '../types' import { isEmpty, prettifyHeaderName, getEmptyStringForUndefined, arrayToPairs } from '../utils' @@ -42,12 +42,12 @@ export class HttpZBaseBuilder { this._processTransferEncodingChunked() switch (this.body!.contentType) { - case HttpContentMultipartType.formData: - case HttpContentMultipartType.alternative: - case HttpContentMultipartType.mixed: - case HttpContentMultipartType.related: + case HttpContentTypeMultipart.formData: + case HttpContentTypeMultipart.alternative: + case HttpContentTypeMultipart.mixed: + case HttpContentTypeMultipart.related: return this._generateFormDataBody() - case HttpContentApplicationType.xWwwFormUrlencoded: + case HttpContentTypeApplication.xWwwFormUrlencoded: return this._generateUrlencodedBody() default: return this._generateTextBody() diff --git a/src/constants.ts b/src/constants.ts index d5d61c8..2d64934 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,24 +2,26 @@ export const EOL = '\r\n' export const EOL2X = EOL + EOL const BASIC_LATIN = '[\\u0009\\u0020-\\u007E]' -const PARAM_NAME = '[A-Za-z0-9_.\\[\\]-]' // TODO: extend const HTTP_METHODS = '(CONNECT|OPTIONS|TRACE|GET|HEAD|POST|PUT|PATCH|DELETE)' const HTTP_PROTOCOL_VERSIONS = '(HTTP)\\/(1\\.0|1\\.1|2(\\.0){0,1})' export const regexps = { quote: /"/g, - startNl: new RegExp(`^${EOL}`), - endNl: new RegExp(`${EOL}$`), - requestStartRow: new RegExp(`^${HTTP_METHODS} \\S* ${HTTP_PROTOCOL_VERSIONS}$`), - responseStartRow: new RegExp(`^${HTTP_PROTOCOL_VERSIONS} \\d{3} ${BASIC_LATIN}*$`), + nlStart: new RegExp(`^${EOL}`), + nlEnd: new RegExp(`${EOL}$`), + requestStartRow: new RegExp(`^${HTTP_METHODS}\\s\\S*\\s${HTTP_PROTOCOL_VERSIONS}$`), + responseStartRow: new RegExp(`^${HTTP_PROTOCOL_VERSIONS}\\s\\d{3}\\s${BASIC_LATIN}*$`), // eslint-disable-next-line no-control-regex quotedHeaderValue: new RegExp('^"[\\u0009\\u0020\\u0021\\u0023-\\u007E]+"$'), - boundary: /(?<=boundary=)"{0,1}[A-Za-z0-9'()+_,.:=?-]+"{0,1}/, - contentDisposition: new RegExp(`^Content-Disposition: *(form-data|inline|attachment)${BASIC_LATIN}*${EOL}`, 'i'), + boundary: new RegExp(`(?<=boundary=)"{0,1}[A-Za-z0-9'()+_,.:=?-]+"{0,1}`), + contentDisposition: new RegExp( + `^Content-Disposition:\\s*(form-data|inline|attachment)(?:\\s*;\\s*(name|filename)\\s*=\\s*(?:"([^"]+)"|([^;\\s]+)))*${EOL}`, + 'i', + ), contentType: new RegExp(`^Content-Type:[\\S ]*${EOL}`, 'i'), - contentDispositionType: /(?<=Content-Disposition:) *(form-data|inline|attachment)/, - dispositionName: new RegExp(`(?<=name=)"${PARAM_NAME}+"`, 'i'), - dispositionFileName: new RegExp(`(?<=filename=)"${PARAM_NAME}+"`, 'i'), + contentDispositionType: new RegExp(`(?<=Content-Disposition:)\\s*(form-data|inline|attachment)`), + dispositionName: new RegExp(`(?<=name=)(?:"([^"]+)"|([^;\\s]+))+`, 'i'), + dispositionFileName: new RegExp(`(?<=filename=)(?:"([^"]+)"|([^;\\s]+))+`, 'i'), chunkRow: new RegExp(`^[0-9a-fA-F]+${EOL}`), } @@ -57,7 +59,7 @@ export enum HttpHeader { transferEncoding = 'Transfer-Encoding', } -export enum HttpContentTextType { +export enum HttpContentTypeText { any = 'text/', css = 'text/css', csv = 'text/csv', @@ -67,7 +69,7 @@ export enum HttpContentTextType { xml = 'text/xml', } -export enum HttpContentApplicationType { +export enum HttpContentTypeApplication { any = 'application/', javascript = 'application/javascript', json = 'application/json', @@ -81,7 +83,7 @@ export enum HttpContentApplicationType { zip = 'application/zip', } -export enum HttpContentMultipartType { +export enum HttpContentTypeMultipart { any = 'multipart/', alternative = 'multipart/alternative', formData = 'multipart/form-data', @@ -89,7 +91,7 @@ export enum HttpContentMultipartType { related = 'multipart/related', } -export enum HttpContentImageType { +export enum HttpContentTypeImage { any = 'image/', gif = 'image/gif', jpeg = 'image/jpeg', @@ -98,14 +100,14 @@ export enum HttpContentImageType { icon = 'image/x-icon', } -export enum HttpContentAudioType { +export enum HttpContentTypeAudio { any = 'audio/', } -export enum HttpContentVideoType { +export enum HttpContentTypeVideo { any = 'video/', } -export enum HttpContentFonType { +export enum HttpContentTypeFont { any = 'font/', } diff --git a/src/parsers/base.ts b/src/parsers/base.ts index 836e5bc..14cd95f 100644 --- a/src/parsers/base.ts +++ b/src/parsers/base.ts @@ -1,4 +1,4 @@ -import { EOL, EOL2X, HttpContentApplicationType, HttpContentMultipartType, HttpHeader, regexps } from '../constants' +import { EOL, EOL2X, HttpContentTypeApplication, HttpContentTypeMultipart, HttpHeader, regexps } from '../constants' import { HttpZError } from '../error' import { HttpZBody, HttpZHeader } from '../types' import { splitBy, prettifyHeaderName, head, tail, isNil, trim } from '../utils' @@ -67,13 +67,13 @@ export class HttpZBaseParser { this.body.contentType = contentTypeHeader.toLowerCase().split(';')[0] } switch (this.body.contentType) { - case HttpContentMultipartType.formData: - case HttpContentMultipartType.alternative: - case HttpContentMultipartType.mixed: - case HttpContentMultipartType.related: + case HttpContentTypeMultipart.formData: + case HttpContentTypeMultipart.alternative: + case HttpContentTypeMultipart.mixed: + case HttpContentTypeMultipart.related: this._parseFormDataBody() break - case HttpContentApplicationType.xWwwFormUrlencoded: + case HttpContentTypeApplication.xWwwFormUrlencoded: this._parseUrlencodedBody() break default: diff --git a/src/parsers/form-data-param-parser.ts b/src/parsers/form-data-param-parser.ts index c4a2e05..60ea243 100644 --- a/src/parsers/form-data-param-parser.ts +++ b/src/parsers/form-data-param-parser.ts @@ -14,7 +14,7 @@ export class FormDataParamParser { // TODO: test it parse(): HttpZBodyParam { - this.paramGroup = this.paramGroup.replace(regexps.startNl, '').replace(regexps.endNl, '') + this.paramGroup = this.paramGroup.replace(regexps.nlStart, '').replace(regexps.nlEnd, '') const contentDispositionHeader = this._getContentDisposition() const contentType = this._getContentType() @@ -89,8 +89,8 @@ export class FormDataParamParser { // TODO: test it private _getParamValue(): string | never { - if (this.paramGroup.match(regexps.startNl)) { - return this.paramGroup.replace(regexps.startNl, '') + if (this.paramGroup.match(regexps.nlStart)) { + return this.paramGroup.replace(regexps.nlStart, '') } throw HttpZError.get('Incorrect form-data parameter', this.paramGroup) } diff --git a/test/parsers/request.spec.ts b/test/parsers/request.spec.ts index fcfdb80..17d4b16 100644 --- a/test/parsers/request.spec.ts +++ b/test/parsers/request.spec.ts @@ -703,7 +703,7 @@ describe('parsers / request', () => { '', 'John', '--111362:53119209', - 'Content-Disposition: form-data; name="photo"; filename="photo1.jpg"', + 'Content-Disposition: form-data; name="photo-㡣"; filename="photo-㡣1.jpg"', 'Content-Type: application/octet-stream', '', '', @@ -765,8 +765,8 @@ describe('parsers / request', () => { { name: 'user.data[firstName]', value: 'John' }, { contentType: 'application/octet-stream', - name: 'photo', - fileName: 'photo1.jpg', + name: 'photo-㡣', + fileName: 'photo-㡣1.jpg', value: '', }, { @@ -777,7 +777,7 @@ describe('parsers / request', () => { ], }, headersSize: 284, - bodySize: 367, + bodySize: 371, } const parser = getParserInstance(rawRequest) @@ -877,7 +877,7 @@ describe('parsers / request', () => { 'Content-Length: 301', '', '--11136253119209', - 'Content-Disposition: attachment; filename="photo1.jpg"', + 'Content-Disposition: attachment; filename="photo-㡣1.jpg"', 'Content-Type: application/octet-stream', '', '', @@ -932,13 +932,13 @@ describe('parsers / request', () => { { type: 'attachment', contentType: 'application/octet-stream', - fileName: 'photo1.jpg', + fileName: 'photo-㡣1.jpg', value: '', }, ], }, headersSize: 279, - bodySize: 149, + bodySize: 151, } const parser = getParserInstance(rawRequest) From f7ab22b086ac3ca1c62e11b774134caeba5b69a9 Mon Sep 17 00:00:00 2001 From: Alexander Mac Date: Wed, 19 Mar 2025 10:41:04 +0300 Subject: [PATCH 2/2] add HTTP/3 protocol version --- src/constants.ts | 8 ++++---- test/parsers/request.spec.ts | 15 ++++++++++++--- test/parsers/response.spec.ts | 8 ++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2d64934..1545993 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,16 +1,15 @@ export const EOL = '\r\n' export const EOL2X = EOL + EOL -const BASIC_LATIN = '[\\u0009\\u0020-\\u007E]' const HTTP_METHODS = '(CONNECT|OPTIONS|TRACE|GET|HEAD|POST|PUT|PATCH|DELETE)' -const HTTP_PROTOCOL_VERSIONS = '(HTTP)\\/(1\\.0|1\\.1|2(\\.0){0,1})' +const HTTP_PROTOCOL_VERSIONS = '(HTTP)\\/(1\\.0|1\\.1|2(\\.0){0,1}|3(\\.0){0,1})' export const regexps = { quote: /"/g, nlStart: new RegExp(`^${EOL}`), nlEnd: new RegExp(`${EOL}$`), requestStartRow: new RegExp(`^${HTTP_METHODS}\\s\\S*\\s${HTTP_PROTOCOL_VERSIONS}$`), - responseStartRow: new RegExp(`^${HTTP_PROTOCOL_VERSIONS}\\s\\d{3}\\s${BASIC_LATIN}*$`), + responseStartRow: new RegExp(`^${HTTP_PROTOCOL_VERSIONS}\\s\\d{3}\\s[^\r\n]*$`), // eslint-disable-next-line no-control-regex quotedHeaderValue: new RegExp('^"[\\u0009\\u0020\\u0021\\u0023-\\u007E]+"$'), boundary: new RegExp(`(?<=boundary=)"{0,1}[A-Za-z0-9'()+_,.:=?-]+"{0,1}`), @@ -33,7 +32,8 @@ export enum HttpProtocol { export enum HttpProtocolVersion { http10 = 'HTTP/1.0', http11 = 'HTTP/1.1', - http20 = 'HTTP/2.0', + http2 = 'HTTP/2', + http3 = 'HTTP/3', } export enum HttpMethod { diff --git a/test/parsers/request.spec.ts b/test/parsers/request.spec.ts index 17d4b16..78e5128 100644 --- a/test/parsers/request.spec.ts +++ b/test/parsers/request.spec.ts @@ -212,10 +212,19 @@ describe('parsers / request', () => { test(startRow, expected) }) - it('should parse valid startRow when HTTP protocol is v2.0', () => { - const startRow = 'GET /features HTTP/2.0' + it('should parse valid startRow when HTTP protocol is v2', () => { + const startRow = 'GET /features HTTP/2' const expected = getExpected({ - protocolVersion: HttpProtocolVersion.http20, + protocolVersion: HttpProtocolVersion.http2, + }) + + test(startRow, expected) + }) + + it('should parse valid startRow when HTTP protocol is v3', () => { + const startRow = 'GET /features HTTP/3' + const expected = getExpected({ + protocolVersion: HttpProtocolVersion.http3, }) test(startRow, expected) diff --git a/test/parsers/response.spec.ts b/test/parsers/response.spec.ts index 20732d8..3e6a8ac 100644 --- a/test/parsers/response.spec.ts +++ b/test/parsers/response.spec.ts @@ -124,12 +124,12 @@ describe('parsers / response', () => { it('should set instance fields when startRow has valid format (reason is not empty)', () => { const parser = getParserInstance() - parser['startRow'] = 'HTTP/1.1 201 Created' + parser['startRow'] = 'HTTP/3 500 Server Error' parser['_parseStartRow']() - expect(parser['protocolVersion']).toEqual('HTTP/1.1') - expect(parser['statusCode']).toEqual(201) - expect(parser['statusMessage']).toEqual('Created') + expect(parser['protocolVersion']).toEqual('HTTP/3') + expect(parser['statusCode']).toEqual(500) + expect(parser['statusMessage']).toEqual('Server Error') }) })