Skip to content

Commit c351e34

Browse files
lholmquistdeewhyweboisinisapotatoesnyk-botlance
authored
3 more commits for the backport (#2)
* chore(example): Replaced body parser with express JSON parser (cloudevents#334) Signed-off-by: Philip Hayes <[email protected]> Co-authored-by: Philip Hayes <[email protected]> * fix: upgrade cloudevents from 3.0.1 to 3.1.0 (cloudevents#335) Snyk has created this PR to upgrade cloudevents from 3.0.1 to 3.1.0. See this package in npm: https://www.npmjs.com/package/cloudevents See this project in Snyk: https://app.snyk.io/org/lance/project/cb2960b0-db0c-4e77-9ab2-e78efded812e?utm_source=github&utm_medium=upgrade-pr Co-authored-by: snyk-bot <[email protected]> Signed-off-by: Lucas Holmquist <[email protected]> * feat: add a constructor parameter for loose validation (cloudevents#328) * feat: add a constructor parameter for loose validation This commit adds a second, optional boolean parameter to the `CloudEvent` constructor. When `false` is provided, the event constructor will not perform validation of the event properties, values and extension names. This commit also modifies the ValidationError class so that the error message string includes the JSON.stringified version of any schema validation errors. It also makes the HTTP.toEvent() function create CloudEvent objects with loose/no validation. Incorporates comments from cloudevents#328 Fixes: cloudevents#325 Signed-off-by: Lance Ball <[email protected]> Co-authored-by: Philip Hayes <[email protected]> Co-authored-by: Philip Hayes <[email protected]> Co-authored-by: snyk-bot <[email protected]> Co-authored-by: Lance Ball <[email protected]>
1 parent f3953a9 commit c351e34

File tree

9 files changed

+102
-70
lines changed

9 files changed

+102
-70
lines changed

examples/express-ex/index.js

+2-14
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,8 @@
33
const express = require("express");
44
const { Receiver } = require("cloudevents");
55
const app = express();
6-
7-
app.use((req, res, next) => {
8-
let data = "";
9-
10-
req.setEncoding("utf8");
11-
req.on("data", function (chunk) {
12-
data += chunk;
13-
});
14-
15-
req.on("end", function () {
16-
req.body = data;
17-
next();
18-
});
19-
});
6+
const bodyParser = require('body-parser')
7+
app.use(bodyParser.json())
208

219
app.post("/", (req, res) => {
2210
console.log("HEADERS", req.headers);

examples/express-ex/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"author": "[email protected]",
1515
"license": "Apache-2.0",
1616
"dependencies": {
17+
"body-parser": "^1.19.0",
1718
"cloudevents": "^3.1.0",
1819
"express": "^4.17.1"
1920
}

examples/typescript-ex/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@
2828
"typescript": "~3.9.5"
2929
},
3030
"dependencies": {
31-
"cloudevents": "~3.0.1"
31+
"cloudevents": "~3.1.0"
3232
}
3333
}

src/event/cloudevent.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,15 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
4646
schemaurl?: string;
4747
datacontentencoding?: string;
4848

49-
constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes) {
49+
/**
50+
* Creates a new CloudEvent object with the provided properties. If there is a chance that the event
51+
* properties will not conform to the CloudEvent specification, you may pass a boolean `false` as a
52+
* second parameter to bypass event validation.
53+
*
54+
* @param {object} event the event properties
55+
* @param {boolean?} strict whether to perform event validation when creating the object - default: true
56+
*/
57+
constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes, strict = true) {
5058
// copy the incoming event so that we can delete properties as we go
5159
// everything left after we have deleted know properties becomes an extension
5260
const properties = { ...event };
@@ -105,20 +113,20 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
105113
for (const [key, value] of Object.entries(properties)) {
106114
// Extension names should only allow lowercase a-z and 0-9 in the name
107115
// names should not exceed 20 characters in length
108-
if (!key.match(/^[a-z0-9]{1,20}$/)) {
116+
if (!key.match(/^[a-z0-9]{1,20}$/) && strict) {
109117
throw new ValidationError("invalid extension name");
110118
}
111119

112120
// Value should be spec compliant
113121
// https://github.com/cloudevents/spec/blob/master/spec.md#type-system
114-
if (!isValidType(value)) {
122+
if (!isValidType(value) && strict) {
115123
throw new ValidationError("invalid extension value");
116124
}
117125

118126
this[key] = value;
119127
}
120128

121-
this.validate();
129+
strict ? this.validate() : undefined;
122130

123131
Object.freeze(this);
124132
}
@@ -193,6 +201,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
193201
/**
194202
* Clone a CloudEvent with new/update attributes
195203
* @param {object} options attributes to augment the CloudEvent with
204+
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
196205
* @throws if the CloudEvent does not conform to the schema
197206
* @return {CloudEvent} returns a new CloudEvent
198207
*/
@@ -204,7 +213,8 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
204213
| CloudEventV03
205214
| CloudEventV03Attributes
206215
| CloudEventV03OptionalAttributes,
216+
strict = true,
207217
): CloudEvent {
208-
return new CloudEvent(Object.assign({}, this.toJSON(), options) as CloudEvent);
218+
return new CloudEvent(Object.assign({}, this.toJSON(), options) as CloudEvent, strict);
209219
}
210220
}

src/event/validation.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@ export class ValidationError extends TypeError {
88
errors?: string[] | ErrorObject[] | null;
99

1010
constructor(message: string, errors?: string[] | ErrorObject[] | null) {
11-
super(message);
11+
const messageString =
12+
errors instanceof Array
13+
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
14+
// @ts-ignore
15+
errors?.reduce(
16+
(accum: string, err: Record<string, string>) =>
17+
(accum as string).concat(`
18+
${err instanceof Object ? JSON.stringify(err) : err}`),
19+
message,
20+
)
21+
: message;
22+
super(messageString);
1223
this.errors = errors ? errors : [];
1324
}
1425
}

src/message/http/headers.ts

+6-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
2-
import { ValidationError, CloudEvent } from "../..";
2+
import { CloudEvent } from "../..";
33
import { Headers } from "../";
44
import { Version } from "../../event/cloudevent";
55
import CONSTANTS from "../../constants";
@@ -12,35 +12,6 @@ export const requiredHeaders = [
1212
CONSTANTS.CE_HEADERS.SPEC_VERSION,
1313
];
1414

15-
/**
16-
* Validates cloud event headers and their values
17-
* @param {Headers} headers event transport headers for validation
18-
* @throws {ValidationError} if the headers are invalid
19-
* @return {boolean} true if headers are valid
20-
*/
21-
export function validate(headers: Headers): Headers {
22-
const sanitizedHeaders = sanitize(headers);
23-
24-
// if content-type exists, be sure it's an allowed type
25-
const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE];
26-
const noContentType = !allowedContentTypes.includes(contentTypeHeader);
27-
if (contentTypeHeader && noContentType) {
28-
throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]);
29-
}
30-
31-
requiredHeaders
32-
.filter((required: string) => !sanitizedHeaders[required])
33-
.forEach((required: string) => {
34-
throw new ValidationError(`header '${required}' not found`);
35-
});
36-
37-
if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) {
38-
sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
39-
}
40-
41-
return sanitizedHeaders;
42-
}
43-
4415
/**
4516
* Returns the HTTP headers that will be sent for this event when the HTTP transmission
4617
* mode is "binary". Events sent over HTTP in structured mode only have a single CE header
@@ -89,6 +60,11 @@ export function sanitize(headers: Headers): Headers {
8960
.filter((header) => Object.hasOwnProperty.call(headers, header))
9061
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));
9162

63+
// If no content-type header is sent, assume application/json
64+
if (!sanitized[CONSTANTS.HEADER_CONTENT_TYPE]) {
65+
sanitized[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
66+
}
67+
9268
return sanitized;
9369
}
9470

src/message/http/index.ts

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
22
import { Message, Headers } from "..";
33

4-
import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers, validate } from "./headers";
4+
import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers";
55
import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
6-
import { validateCloudEvent } from "../../event/spec";
76
import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
87

98
// implements Serializer
@@ -129,7 +128,7 @@ function parseBinary(message: Message, version: Version): CloudEvent {
129128
body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body;
130129

131130
// Clone and low case all headers names
132-
const sanitizedHeaders = validate(headers);
131+
const sanitizedHeaders = sanitize(headers);
133132

134133
const eventObj: { [key: string]: unknown | string | Record<string, unknown> } = {};
135134
const parserMap: Record<string, MappedParser> = version === Version.V1 ? v1binaryParsers : v1binaryParsers;
@@ -165,9 +164,7 @@ function parseBinary(message: Message, version: Version): CloudEvent {
165164
delete eventObj.datacontentencoding;
166165
}
167166

168-
const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03);
169-
validateCloudEvent(cloudevent);
170-
return cloudevent;
167+
return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false);
171168
}
172169

173170
/**
@@ -226,9 +223,5 @@ function parseStructured(message: Message, version: Version): CloudEvent {
226223
delete eventObj.data_base64;
227224
delete eventObj.datacontentencoding;
228225
}
229-
const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03);
230-
231-
// Validates the event
232-
validateCloudEvent(cloudevent);
233-
return cloudevent;
226+
return new CloudEvent(eventObj as CloudEventV1 | CloudEventV03, false);
234227
}

test/integration/cloud_event_test.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from "chai";
2-
import { CloudEvent, Version } from "../../src";
2+
import { CloudEvent, ValidationError, Version } from "../../src";
33
import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces";
44

55
const type = "org.cncf.cloudevents.example";
@@ -11,6 +11,7 @@ const fixture: CloudEventV1 = {
1111
specversion: Version.V1,
1212
source,
1313
type,
14+
data: `"some data"`,
1415
};
1516

1617
describe("A CloudEvent", () => {
@@ -20,6 +21,21 @@ describe("A CloudEvent", () => {
2021
expect(ce.source).to.equal(source);
2122
});
2223

24+
it("Can be constructed with loose validation", () => {
25+
const ce = new CloudEvent({} as CloudEventV1, false);
26+
expect(ce).to.be.instanceOf(CloudEvent);
27+
});
28+
29+
it("Loosely validated events can be cloned", () => {
30+
const ce = new CloudEvent({} as CloudEventV1, false);
31+
expect(ce.cloneWith({}, false)).to.be.instanceOf(CloudEvent);
32+
});
33+
34+
it("Loosely validated events throw when validated", () => {
35+
const ce = new CloudEvent({} as CloudEventV1, false);
36+
expect(ce.validate).to.throw(ValidationError, "invalid payload");
37+
});
38+
2339
it("serializes as JSON with toString()", () => {
2440
const ce = new CloudEvent(fixture);
2541
expect(ce.toString()).to.deep.equal(JSON.stringify(ce));
@@ -152,7 +168,7 @@ describe("A 1.0 CloudEvent", () => {
152168
});
153169
} catch (err) {
154170
expect(err).to.be.instanceOf(TypeError);
155-
expect(err.message).to.equal("invalid payload");
171+
expect(err.message).to.include("invalid payload");
156172
}
157173
});
158174

@@ -235,8 +251,8 @@ describe("A 0.3 CloudEvent", () => {
235251
source: (null as unknown) as string,
236252
});
237253
} catch (err) {
238-
expect(err).to.be.instanceOf(TypeError);
239-
expect(err.message).to.equal("invalid payload");
254+
expect(err).to.be.instanceOf(ValidationError);
255+
expect(err.message).to.include("invalid payload");
240256
}
241257
});
242258

test/integration/message_test.ts

+41-4
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,21 @@ const ext2Value = "acme";
2727
const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
2828
const data_base64 = asBase64(dataBinary);
2929

30-
describe("HTTP transport messages", () => {
31-
it("can detect CloudEvent Messages", () => {
30+
describe("HTTP transport", () => {
31+
it("Can detect invalid CloudEvent Messages", () => {
3232
// Create a message that is not an actual event
33-
let message: Message = {
33+
const message: Message = {
3434
body: "Hello world!",
3535
headers: {
3636
"Content-type": "text/plain",
3737
},
3838
};
3939
expect(HTTP.isEvent(message)).to.be.false;
40+
});
4041

42+
it("Can detect valid CloudEvent Messages", () => {
4143
// Now create a message that is an event
42-
message = HTTP.binary(
44+
const message = HTTP.binary(
4345
new CloudEvent({
4446
source: "/message-test",
4547
type: "example",
@@ -48,6 +50,41 @@ describe("HTTP transport messages", () => {
4850
expect(HTTP.isEvent(message)).to.be.true;
4951
});
5052

53+
// Allow for external systems to send bad events - do what we can
54+
// to accept them
55+
it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => {
56+
const message: Message = {
57+
body: `"hello world"`,
58+
headers: {
59+
"content-type": "application/json",
60+
"ce-id": "1234",
61+
"ce-type": "example.bad.event",
62+
"ce-specversion": "1.0",
63+
// no required ce-source header, thus an invalid event
64+
},
65+
};
66+
const event = HTTP.toEvent(message);
67+
expect(event).to.be.instanceOf(CloudEvent);
68+
// ensure that we actually now have an invalid event
69+
expect(event.validate).to.throw;
70+
});
71+
72+
it("Does not allow an invalid CloudEvent to be converted to a Message", () => {
73+
const badEvent = new CloudEvent(
74+
{
75+
source: "/example.source",
76+
type: "", // type is required, empty string will throw with strict validation
77+
},
78+
false, // turn off strict validation
79+
);
80+
expect(() => {
81+
HTTP.binary(badEvent);
82+
}).to.throw;
83+
expect(() => {
84+
HTTP.structured(badEvent);
85+
}).to.throw;
86+
});
87+
5188
describe("Specification version V1", () => {
5289
const fixture: CloudEvent = new CloudEvent({
5390
specversion: Version.V1,

0 commit comments

Comments
 (0)