Skip to content

Commit

Permalink
Refactor usages of Node.js Buffer (#1650)
Browse files Browse the repository at this point in the history
* [wip]

* Passes tests, uploads files

* Clean up

* instanceof Uint8Array in flattenAndStringify

* Add webhook test

* reviewer comments pt. 1

* Add tests for Uint8Array

* add type to push(l)

* Reviewer comments round 2

* Update WebhookHeader and WebhookPayload types
  • Loading branch information
anniel-stripe committed Jan 12, 2023
1 parent b91b541 commit 530c365
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 66 deletions.
27 changes: 17 additions & 10 deletions lib/Webhooks.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 10 additions & 7 deletions lib/multipart.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 23 additions & 5 deletions lib/utils.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions src/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,14 @@ class StripeConnectionError extends StripeError {}
* webhook fails
*/
class StripeSignatureVerificationError extends StripeError {
header: string;
payload: string;

constructor(header: string, payload: string, raw: StripeRawError = {}) {
header: string | Uint8Array;
payload: string | Uint8Array;

constructor(
header: string | Uint8Array,
payload: string | Uint8Array,
raw: StripeRawError = {}
) {
super(raw);
this.header = header;
this.payload = payload;
Expand Down
2 changes: 1 addition & 1 deletion src/Types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
type AppInfo = {name?: string} & Record<string, unknown>;
type BufferedFile = {name: string; type: string; file: {data: Buffer}};
type BufferedFile = {name: string; type: string; file: {data: Uint8Array}};
type HttpClientResponseError = {code: number};
type HttpHeaderValue = string | number | string[];
type MethodSpec = {
Expand Down
39 changes: 23 additions & 16 deletions src/Webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import utils = require('./utils');
import _Error = require('./Error');
const {StripeError, StripeSignatureVerificationError} = _Error;

type WebhookHeader = string | Buffer;
type WebhookHeader = string | Uint8Array;
type WebhookParsedHeader = {
signatures: Array<string>;
timestamp: number;
};
type WebhookParsedEvent = {
details: WebhookParsedHeader;
decodedPayload: string;
decodedHeader: string;
decodedPayload: WebhookHeader;
decodedHeader: WebhookPayload;
};
type WebhookTestHeaderOptions = {
timestamp: number;
Expand All @@ -22,7 +22,7 @@ type WebhookTestHeaderOptions = {
};

type WebhookEvent = Record<string, unknown>;
type WebhookPayload = string | Buffer;
type WebhookPayload = string | Uint8Array;
type WebhookSignatureObject = {
verifyHeader: (
encodedPayload: WebhookPayload,
Expand Down Expand Up @@ -78,8 +78,10 @@ const Webhook: WebhookObject = {
cryptoProvider
);

// @ts-ignore
const jsonPayload = JSON.parse(payload);
const jsonPayload =
payload instanceof Uint8Array
? JSON.parse(new TextDecoder('utf8').decode(payload))
: JSON.parse(payload);
return jsonPayload;
},

Expand All @@ -98,8 +100,10 @@ const Webhook: WebhookObject = {
cryptoProvider
);

// @ts-ignore
const jsonPayload = JSON.parse(payload);
const jsonPayload =
payload instanceof Uint8Array
? JSON.parse(new TextDecoder('utf8').decode(payload))
: JSON.parse(payload);
return jsonPayload;
},

Expand Down Expand Up @@ -218,9 +222,11 @@ function parseEventDetails(
encodedHeader: WebhookHeader,
expectedScheme: string
): WebhookParsedEvent {
const decodedPayload = Buffer.isBuffer(encodedPayload)
? encodedPayload.toString('utf8')
: encodedPayload;
const textDecoder = new TextDecoder('utf8');
const decodedPayload =
encodedPayload instanceof Uint8Array
? textDecoder.decode(encodedPayload)
: encodedPayload;

// Express's type for `Request#headers` is `string | []string`
// which is because the `set-cookie` header is an array,
Expand All @@ -232,9 +238,10 @@ function parseEventDetails(
);
}

const decodedHeader = Buffer.isBuffer(encodedHeader)
? encodedHeader.toString('utf8')
: encodedHeader;
const decodedHeader =
encodedHeader instanceof Uint8Array
? textDecoder.decode(encodedHeader)
: encodedHeader;

const details = parseHeader(decodedHeader, expectedScheme);

Expand All @@ -259,7 +266,7 @@ function parseEventDetails(

function validateComputedSignature(
payload: WebhookPayload,
header: string,
header: WebhookHeader,
details: WebhookParsedHeader,
expectedSignature: string,
tolerance: number
Expand Down Expand Up @@ -292,7 +299,7 @@ function validateComputedSignature(
}

function parseHeader(
header: string,
header: WebhookHeader,
scheme: string
): WebhookParsedHeader | null {
if (typeof header !== 'string') {
Expand Down
31 changes: 18 additions & 13 deletions src/multipart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class StreamProcessingError extends StripeError {}
type MultipartCallbackReturn = any;
type MultipartCallback = (
error: Error | null,
data: Buffer | string | null
data: Uint8Array | string | null
) => MultipartCallbackReturn;
// Method for formatting HTTP body for the multipart/form-data specification
// Mostly taken from Fermata.js
Expand All @@ -15,20 +15,25 @@ const multipartDataGenerator = (
method: string,
data: MultipartRequestData,
headers: RequestHeaders
): Buffer => {
): Uint8Array => {
const segno = (
Math.round(Math.random() * 1e16) + Math.round(Math.random() * 1e16)
).toString();
headers['Content-Type'] = `multipart/form-data; boundary=${segno}`;
let buffer = Buffer.alloc(0);
const textEncoder = new TextEncoder();

function push(l: any): void {
let buffer = new Uint8Array(0);
const endBuffer = textEncoder.encode('\r\n');

function push(l: string | Uint8Array): void {
const prevBuffer = buffer;
const newBuffer = l instanceof Buffer ? l : Buffer.from(l);
buffer = Buffer.alloc(prevBuffer.length + newBuffer.length + 2);
prevBuffer.copy(buffer);
newBuffer.copy(buffer, prevBuffer.length);
buffer.write('\r\n', buffer.length - 2);
const newBuffer =
l instanceof Uint8Array ? l : new Uint8Array(textEncoder.encode(l));
buffer = new Uint8Array(prevBuffer.length + newBuffer.length + 2);

buffer.set(prevBuffer);
buffer.set(newBuffer, prevBuffer.length);
buffer.set(endBuffer, buffer.length - 2);
}

function q(s: string): string {
Expand All @@ -43,7 +48,7 @@ const multipartDataGenerator = (
if (Object.prototype.hasOwnProperty.call(v, 'data')) {
const typedEntry: {
name: string;
data: BufferedFile;
data: string | Uint8Array;
type: string;
} = v as any;
push(
Expand Down Expand Up @@ -71,15 +76,15 @@ const streamProcessor = (
headers: RequestHeaders,
callback: MultipartCallback
): void => {
const bufferArray: Array<Buffer> = [];
const bufferArray: Array<Uint8Array> = [];
data.file.data
.on('data', (line: Buffer) => {
.on('data', (line: Uint8Array) => {
bufferArray.push(line);
})
.once('end', () => {
// @ts-ignore
const bufferData: BufferedFile = Object.assign({}, data);
bufferData.file.data = Buffer.concat(bufferArray);
bufferData.file.data = utils.concat(bufferArray);
const buffer = multipartDataGenerator(method, bufferData, headers);
callback(null, buffer);
})
Expand Down

0 comments on commit 530c365

Please sign in to comment.