From 16ef86acef515342903a0c3ab668f40223892e77 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Tue, 12 Jan 2021 15:40:17 +0100 Subject: [PATCH] fix: Allow Content-Type: 0 on GET. Fixes https://github.com/solid/community-server/issues/498 --- src/ldp/http/RawBodyParser.ts | 18 ++++++++---- test/integration/ServerWithAuth.test.ts | 27 ++++++++++++++++++ test/unit/ldp/http/RawBodyParser.test.ts | 35 ++++++++++++++++++------ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/ldp/http/RawBodyParser.ts b/src/ldp/http/RawBodyParser.ts index 57ac98d44..b3864e63a 100644 --- a/src/ldp/http/RawBodyParser.ts +++ b/src/ldp/http/RawBodyParser.ts @@ -14,18 +14,26 @@ export class RawBodyParser extends BodyParser { // Note that the only reason this is a union is in case the body is empty. // If this check gets moved away from the BodyParsers this union could be removed public async handle({ request, metadata }: BodyParserArgs): Promise { + const { + 'content-type': contentType, + 'content-length': contentLength, + 'transfer-encoding': transferEncoding, + } = request.headers; + // RFC7230, ยง3.3: The presence of a message body in a request // is signaled by a Content-Length or Transfer-Encoding header field. - if (!request.headers['content-length'] && !request.headers['transfer-encoding']) { - this.logger.debug('HTTP request appears to not have a body, so nothing to parse'); + // While clients SHOULD NOT use use a Content-Length header on GET, + // some still provide a Content-Length of 0 (but without Content-Type). + if ((!contentLength || (/^0+$/u.test(contentLength) && !contentType)) && !transferEncoding) { + this.logger.debug('HTTP request does not have a body, or its empty body is missing a Content-Type header'); return; } // While RFC7231 allows treating a body without content type as an octet stream, // such an omission likely signals a mistake, so force clients to make this explicit. - if (!request.headers['content-type']) { - this.logger.warn('A body was passed, but the content length was not specified'); - throw new BadRequestHttpError('HTTP request body was passed without Content-Type header'); + if (!contentType) { + this.logger.warn('HTTP request has a body, but no Content-Type header'); + throw new BadRequestHttpError('HTTP request body was passed without a Content-Type header'); } return new BasicRepresentation(request, metadata); diff --git a/test/integration/ServerWithAuth.test.ts b/test/integration/ServerWithAuth.test.ts index cdcb95b6d..145d995a9 100644 --- a/test/integration/ServerWithAuth.test.ts +++ b/test/integration/ServerWithAuth.test.ts @@ -86,4 +86,31 @@ describe('A server with authorization', (): void => { ); expect(response.statusCode).toBe(401); }); + + // https://github.com/solid/community-server/issues/498 + it('accepts a GET with Content-Length: 0.', async(): Promise => { + await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent'); + + // PUT + let requestUrl = new URL('http://test.com/foo/bar'); + let response: MockResponse = await performRequest( + handler, + requestUrl, + 'PUT', + { 'content-length': '0', 'content-type': 'text/turtle' }, + [], + ); + expect(response.statusCode).toBe(205); + + // GET + requestUrl = new URL('http://test.com/foo/bar'); + response = await performRequest( + handler, + requestUrl, + 'GET', + { 'content-length': '0' }, + [], + ); + expect(response.statusCode).toBe(200); + }); }); diff --git a/test/unit/ldp/http/RawBodyParser.test.ts b/test/unit/ldp/http/RawBodyParser.test.ts index 15a236b4a..d6ab85341 100644 --- a/test/unit/ldp/http/RawBodyParser.test.ts +++ b/test/unit/ldp/http/RawBodyParser.test.ts @@ -18,27 +18,46 @@ describe('A RawBodyparser', (): void => { await expect(bodyParser.canHandle({} as any)).resolves.toBeUndefined(); }); - it('returns empty output if there was no content length or transfer encoding.', async(): Promise => { + it('returns empty output if there is no content length or transfer encoding.', async(): Promise => { input.request = streamifyArray([ '' ]) as HttpRequest; input.request.headers = {}; await expect(bodyParser.handle(input)).resolves.toBeUndefined(); }); - it('errors when a content length was specified without content type.', async(): Promise => { - input.request = streamifyArray([ 'abc' ]) as HttpRequest; + // https://github.com/solid/community-server/issues/498 + it('returns empty output if the content length is 0 and there is no content type.', async(): Promise => { + input.request = streamifyArray([ '' ]) as HttpRequest; input.request.headers = { 'content-length': '0' }; - await expect(bodyParser.handle(input)).rejects - .toThrow('HTTP request body was passed without Content-Type header'); + await expect(bodyParser.handle(input)).resolves.toBeUndefined(); }); - it('errors when a transfer encoding was specified without content type.', async(): Promise => { + it('errors when a content length is specified without content type.', async(): Promise => { + input.request = streamifyArray([ 'abc' ]) as HttpRequest; + input.request.headers = { 'content-length': '1' }; + await expect(bodyParser.handle(input)).rejects + .toThrow('HTTP request body was passed without a Content-Type header'); + }); + + it('errors when a transfer encoding is specified without content type.', async(): Promise => { input.request = streamifyArray([ 'abc' ]) as HttpRequest; input.request.headers = { 'transfer-encoding': 'chunked' }; await expect(bodyParser.handle(input)).rejects - .toThrow('HTTP request body was passed without Content-Type header'); + .toThrow('HTTP request body was passed without a Content-Type header'); }); - it('returns a Representation if there was data.', async(): Promise => { + it('returns a Representation if there is empty data.', async(): Promise => { + input.request = streamifyArray([]) as HttpRequest; + input.request.headers = { 'content-length': '0', 'content-type': 'text/turtle' }; + const result = (await bodyParser.handle(input))!; + expect(result).toEqual({ + binary: true, + data: input.request, + metadata: input.metadata, + }); + await expect(arrayifyStream(result.data)).resolves.toEqual([]); + }); + + it('returns a Representation if there is non-empty data.', async(): Promise => { input.request = streamifyArray([ ' .' ]) as HttpRequest; input.request.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' }; const result = (await bodyParser.handle(input))!;