Skip to content

Commit 624fc79

Browse files
committed
fix: ensure that received binary data remains binary in an event
Unless... its datacontenttype is application/json. In that case it should be converted into an object. Signed-off-by: Lance Ball <[email protected]>
1 parent db40de9 commit 624fc79

File tree

6 files changed

+114
-51
lines changed

6 files changed

+114
-51
lines changed

src/event/cloudevent.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
159159
try {
160160
return validateCloudEvent(this);
161161
} catch (e) {
162-
console.error(e.errors);
163162
if (e instanceof ValidationError) {
164163
throw e;
165164
} else {

src/message/http/index.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } fro
22
import { Message, Headers } from "..";
33

44
import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers";
5-
import { isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
6-
import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
5+
import { isBinary, isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
6+
import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
77

88
// implements Serializer
99
export function binary(event: CloudEvent): Message {
@@ -22,6 +22,10 @@ export function binary(event: CloudEvent): Message {
2222

2323
// implements Serializer
2424
export function structured(event: CloudEvent): Message {
25+
if (event.data_base64) {
26+
// The event's data is binary - delete it
27+
event = event.cloneWith({ data: undefined });
28+
}
2529
return {
2630
headers: {
2731
[CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE,
@@ -130,8 +134,6 @@ function parseBinary(message: Message, version: Version): CloudEvent {
130134
throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`);
131135
}
132136

133-
body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body;
134-
135137
// Clone and low case all headers names
136138
const sanitizedHeaders = sanitize(headers);
137139

@@ -146,32 +148,34 @@ function parseBinary(message: Message, version: Version): CloudEvent {
146148
}
147149
}
148150

149-
let parsedPayload;
150-
151-
if (body) {
152-
const parser = parserByContentType[eventObj.datacontenttype as string];
153-
if (parser) {
154-
parsedPayload = parser.parse(body as string);
155-
} else {
156-
// Just use the raw body
157-
parsedPayload = body;
158-
}
159-
}
160-
161151
// Every unprocessed header can be an extension
162152
for (const header in sanitizedHeaders) {
163153
if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) {
164154
eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header];
165155
}
166156
}
157+
158+
if (isBinary(body)) {
159+
if (eventObj.datacontenttype === CONSTANTS.MIME_JSON) {
160+
// even though the body is binary, it's just JSON - convert to string
161+
body = Buffer.from(body as string, "base64").toString();
162+
}
163+
}
164+
165+
// If the body is base64 that means we are dealing with binary data
166+
const parser = parserByContentType[eventObj.datacontenttype as string];
167+
if (parser && body) {
168+
body = parser.parse(body as string);
169+
}
170+
167171
// At this point, if the datacontenttype is application/json and the datacontentencoding is base64
168172
// then the data has already been decoded as a string, then parsed as JSON. We don't need to have
169173
// the datacontentencoding property set - in fact, it's incorrect to do so.
170174
if (eventObj.datacontenttype === CONSTANTS.MIME_JSON && eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) {
171175
delete eventObj.datacontentencoding;
172176
}
173177

174-
return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false);
178+
return new CloudEvent({ ...eventObj, data: body } as CloudEventV1 | CloudEventV03, false);
175179
}
176180

177181
/**
@@ -223,13 +227,19 @@ function parseStructured(message: Message, version: Version): CloudEvent {
223227
eventObj[key] = incoming[key];
224228
}
225229

226-
// ensure data content is correctly decoded
230+
// data_base64 is a property that only exists on V1 events. For V03 events,
231+
// there will be a .datacontentencoding property, and the .data property
232+
// itself will be encoded as base64
227233
if (eventObj.data_base64 || eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) {
228-
const parser = new Base64Parser();
229-
// data_base64 is a property that only exists on V1 events. For V03 events,
230-
// there will be a .datacontentencoding property, and the .data property
231-
// itself will be encoded as base64
232-
eventObj.data = JSON.parse(parser.parse((eventObj.data_base64 as string) || (eventObj.data as string)));
234+
const data = eventObj.data_base64 || eventObj.data;
235+
236+
// we can choose to decode the binary data as JSON, if datacontentype is application/json
237+
if (eventObj.datacontenttype === CONSTANTS.MIME_JSON) {
238+
eventObj.data = JSON.parse(Buffer.from(data as string, "base64").toString());
239+
} else {
240+
eventObj.data = new Uint32Array(Buffer.from(data as string, "base64"));
241+
}
242+
233243
delete eventObj.data_base64;
234244
delete eventObj.datacontentencoding;
235245
}

src/transport/receiver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const Receiver = {
1717
*/
1818
accept(headers: Headers, body: string | Record<string, unknown> | undefined | null): CloudEvent {
1919
const cleanHeaders: Headers = sanitize(headers);
20-
const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : "";
20+
const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : undefined;
2121
const message: Message = {
2222
headers: cleanHeaders,
2323
body: cleanBody,

test/integration/ce.png

39.8 KB
Loading

test/integration/cloud_event_test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import path from "path";
2+
import fs from "fs";
3+
14
import { expect } from "chai";
25
import { CloudEvent, ValidationError, Version } from "../../src";
36
import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces";
7+
import { asBase64 } from "../../src/event/validation";
48

59
const type = "org.cncf.cloudevents.example";
610
const source = "http://unit.test";
@@ -14,6 +18,9 @@ const fixture: CloudEventV1 = {
1418
data: `"some data"`,
1519
};
1620

21+
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
22+
const image_base64 = asBase64(imageData);
23+
1724
describe("A CloudEvent", () => {
1825
it("Can be constructed with a typed Message", () => {
1926
const ce = new CloudEvent(fixture);
@@ -151,6 +158,15 @@ describe("A 1.0 CloudEvent", () => {
151158
expect(ce.data).to.be.true;
152159
});
153160

161+
it("can be constructed with binary data", () => {
162+
const ce = new CloudEvent({
163+
...fixture,
164+
data: imageData,
165+
});
166+
expect(ce.data).to.equal(imageData);
167+
expect(ce.data_base64).to.equal(image_base64);
168+
});
169+
154170
it("can be constructed with extensions", () => {
155171
const extensions = {
156172
extensionkey: "extension-value",

test/integration/message_test.ts

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import path from "path";
2+
import fs from "fs";
3+
14
import { expect } from "chai";
25
import { CloudEvent, CONSTANTS, Version } from "../../src";
36
import { asBase64 } from "../../src/event/validation";
@@ -16,7 +19,6 @@ const data = {
1619

1720
// Attributes for v03 events
1821
const schemaurl = "https://cloudevents.io/schema.json";
19-
const datacontentencoding = "base64";
2022

2123
const ext1Name = "extension1";
2224
const ext1Value = "foobar";
@@ -25,7 +27,11 @@ const ext2Value = "acme";
2527

2628
// Binary data as base64
2729
const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
28-
const data_base64 = asBase64(dataBinary);
30+
31+
// Since the above is a special case (string as binary), let's test
32+
// with a real binary file one is likely to encounter in the wild
33+
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
34+
const image_base64 = asBase64(imageData);
2935

3036
describe("HTTP transport", () => {
3137
it("Can detect invalid CloudEvent Messages", () => {
@@ -45,6 +51,7 @@ describe("HTTP transport", () => {
4551
new CloudEvent({
4652
source: "/message-test",
4753
type: "example",
54+
data,
4855
}),
4956
);
5057
expect(HTTP.isEvent(message)).to.be.true;
@@ -144,23 +151,47 @@ describe("HTTP transport", () => {
144151
expect(event).to.deep.equal(fixture);
145152
});
146153

147-
it("Supports Base-64 encoded data in structured messages", () => {
148-
const event = fixture.cloneWith({ data: dataBinary });
149-
expect(event.data_base64).to.equal(data_base64);
150-
expect(event.data).to.equal(dataBinary);
154+
it("Converts binary data to base64 when serializing structured messages", () => {
155+
const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" });
156+
expect(event.data).to.equal(imageData);
151157
const message = HTTP.structured(event);
158+
const messageBody = JSON.parse(message.body as string);
159+
expect(messageBody.data_base64).to.equal(image_base64);
160+
});
161+
162+
it("Converts base64 encoded data to binary when deserializing structured messages", () => {
163+
const message = HTTP.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
164+
const eventDeserialized = HTTP.toEvent(message);
165+
expect(eventDeserialized.data).to.deep.equal(imageData);
166+
expect(eventDeserialized.data_base64).to.equal(image_base64);
167+
});
168+
169+
it("Parses binary data from structured messages with content type application/json", () => {
170+
const message = HTTP.structured(fixture.cloneWith({ data: dataBinary }));
152171
const eventDeserialized = HTTP.toEvent(message);
153172
expect(eventDeserialized.data).to.deep.equal({ foo: "bar" });
173+
expect(eventDeserialized.data_base64).to.be.undefined;
174+
});
175+
176+
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
177+
const message = HTTP.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
178+
const eventDeserialized = HTTP.toEvent(message);
179+
expect(eventDeserialized.data).to.deep.equal(imageData);
180+
expect(eventDeserialized.data_base64).to.equal(image_base64);
154181
});
155182

156-
it("Supports Base-64 encoded data in binary messages", () => {
183+
it("Keeps binary data binary when serializing binary messages", () => {
157184
const event = fixture.cloneWith({ data: dataBinary });
158-
expect(event.data_base64).to.equal(data_base64);
159185
expect(event.data).to.equal(dataBinary);
160186
const message = HTTP.binary(event);
161187
expect(message.body).to.equal(dataBinary);
188+
});
189+
190+
it("Parses binary data from binary messages with content type application/json", () => {
191+
const message = HTTP.binary(fixture.cloneWith({ data: dataBinary }));
162192
const eventDeserialized = HTTP.toEvent(message);
163-
expect(eventDeserialized.data).to.equal(dataBinary);
193+
expect(eventDeserialized.data).to.deep.equal({ foo: "bar" });
194+
expect(eventDeserialized.data_base64).to.be.undefined;
164195
});
165196
});
166197

@@ -223,28 +254,35 @@ describe("HTTP transport", () => {
223254
expect(event).to.deep.equal(fixture);
224255
});
225256

226-
it("Supports Base-64 encoded data in structured messages", () => {
227-
const event = fixture.cloneWith({ data: data_base64, datacontentencoding });
257+
it("Converts binary data to base64 when serializing structured messages", () => {
258+
const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" });
259+
expect(event.data).to.equal(imageData);
228260
const message = HTTP.structured(event);
229-
expect(JSON.parse(message.body as string).data).to.equal(data_base64);
230-
// An incoming event with datacontentencoding set to base64,
231-
// and encoded data, should decode the data before setting
232-
// the .data property on the event
261+
const messageBody = JSON.parse(message.body as string);
262+
expect(messageBody.data_base64).to.equal(image_base64);
263+
});
264+
265+
it("Converts base64 encoded data to binary when deserializing structured messages", () => {
266+
// Creating an event with binary data automatically produces base64 encoded data
267+
// which is then set as the 'data' attribute on the message body
268+
const message = HTTP.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
233269
const eventDeserialized = HTTP.toEvent(message);
234-
expect(eventDeserialized.data).to.deep.equal({ foo: "bar" });
235-
expect(eventDeserialized.datacontentencoding).to.be.undefined;
270+
expect(eventDeserialized.data).to.deep.equal(imageData);
271+
expect(eventDeserialized.data_base64).to.equal(image_base64);
236272
});
237273

238-
it("Supports Base-64 encoded data in binary messages", () => {
239-
const event = fixture.cloneWith({ data: data_base64, datacontentencoding });
240-
const message = HTTP.binary(event);
241-
expect(message.body).to.equal(data_base64);
242-
// An incoming event with datacontentencoding set to base64,
243-
// and encoded data, should decode the data before setting
244-
// the .data property on the event
274+
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
275+
const message = HTTP.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
245276
const eventDeserialized = HTTP.toEvent(message);
246-
expect(eventDeserialized.data).to.deep.equal({ foo: "bar" });
247-
expect(eventDeserialized.datacontentencoding).to.be.undefined;
277+
expect(eventDeserialized.data).to.deep.equal(imageData);
278+
expect(eventDeserialized.data_base64).to.equal(image_base64);
279+
});
280+
281+
it("Keeps binary data binary when serializing binary messages", () => {
282+
const event = fixture.cloneWith({ data: dataBinary });
283+
expect(event.data).to.equal(dataBinary);
284+
const message = HTTP.binary(event);
285+
expect(message.body).to.equal(dataBinary);
248286
});
249287
});
250288
});

0 commit comments

Comments
 (0)