Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/imports/server/rest/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', {
const smsDepartment = settings.get<string>('SMS_Default_Omnichannel_Department');
const SMSService = await OmnichannelIntegration.getSmsService(service);

if (!(await SMSService.validateRequest(this.request.clone()))) {
if (!(await SMSService.validateRequest(this.request.clone(), this.bodyParams))) {
return API.v1.failure('Invalid request');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export class Mobex implements ISMSProvider {
};
}

async validateRequest(_request: Request): Promise<boolean> {
async validateRequest(_request: Request, _requestBody: unknown): Promise<boolean> {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,16 @@ export class Twilio implements ISMSProvider {
};
}

async isRequestFromTwilio(signature: string, request: Request): Promise<boolean> {
private getUrl(url: string, siteUrl: string): string {
const baseUrl = new URL(url);
const newUrl = new URL(siteUrl);
baseUrl.protocol = newUrl.protocol;
baseUrl.host = newUrl.host;

return baseUrl.toString();
}

async isRequestFromTwilio(signature: string, request: Request, requestBody: unknown): Promise<boolean> {
const authToken = settings.get<string>('SMS_Twilio_authToken');
let siteUrl = settings.get<string>('Site_Url');
if (siteUrl.endsWith('/')) {
Expand All @@ -256,25 +265,19 @@ export class Twilio implements ISMSProvider {
return false;
}

const twilioUrl = request.url ? `${siteUrl}${request.url}` : `${siteUrl}/api/v1/livechat/sms-incoming/twilio`;

let body = {};
try {
body = await request.json();
// eslint-disable-next-line no-empty
} catch {}
const twilioUrl = request.url ? this.getUrl(request.url, siteUrl) : `${siteUrl}/api/v1/livechat/sms-incoming/twilio`;

return twilio.validateRequest(authToken, signature, twilioUrl, body);
return twilio.validateRequest(authToken, signature, twilioUrl, requestBody as Record<string, any>);
}

async validateRequest(request: Request): Promise<boolean> {
async validateRequest(request: Request, requestBody: unknown): Promise<boolean> {
// We're not getting original twilio requests on CI :p
if (process.env.TEST_MODE === 'true') {
return true;
}
const twilioHeader = request.headers.get('x-twilio-signature') || '';
const twilioSignature = Array.isArray(twilioHeader) ? twilioHeader[0] : twilioHeader;
return this.isRequestFromTwilio(twilioSignature, request);
return this.isRequestFromTwilio(twilioSignature, request, requestBody);
}

error(error: Error & { reason?: string }): SMSProviderResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class Voxtelesys implements ISMSProvider {
};
}

async validateRequest(_request: Request): Promise<boolean> {
async validateRequest(_request: Request, _requestBody: unknown): Promise<boolean> {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ describe('Twilio Request Validation', () => {
return headers[param];
},
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.true;
expect(await twilio.validateRequest(request, requestBody)).to.be.true;
});

it('should validate a request when query string is present', async () => {
Expand All @@ -109,7 +108,7 @@ describe('Twilio Request Validation', () => {
};

const request = {
url: '/api/v1/livechat/sms-incoming/twilio?department=1',
url: 'https://example.com/api/v1/livechat/sms-incoming/twilio?department=1',
headers: {
get: (param: string) => {
const headers: Record<string, any> = {
Expand All @@ -119,10 +118,9 @@ describe('Twilio Request Validation', () => {
return headers[param];
},
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.true;
expect(await twilio.validateRequest(request, requestBody)).to.be.true;
});

it('should reject a request where signature doesnt match', async () => {
Expand All @@ -146,10 +144,9 @@ describe('Twilio Request Validation', () => {
return headers[param];
},
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.false;
expect(await twilio.validateRequest(request, requestBody)).to.be.false;
});

it('should reject a request where signature is missing', async () => {
Expand All @@ -167,10 +164,9 @@ describe('Twilio Request Validation', () => {
headers: {
get: () => null,
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.false;
expect(await twilio.validateRequest(request, requestBody)).to.be.false;
});

it('should reject a request where the signature doesnt correspond body', async () => {
Expand All @@ -194,10 +190,9 @@ describe('Twilio Request Validation', () => {
return headers[param];
},
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.false;
expect(await twilio.validateRequest(request, requestBody)).to.be.false;
});

it('should return false if URL is not provided', async () => {
Expand All @@ -223,10 +218,9 @@ describe('Twilio Request Validation', () => {
return headers[param];
},
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.false;
expect(await twilio.validateRequest(request, requestBody)).to.be.false;
});

it('should return false if authToken is not provided', async () => {
Expand All @@ -252,9 +246,38 @@ describe('Twilio Request Validation', () => {
return headers[param];
},
},
json: () => requestBody,
};

expect(await twilio.validateRequest(request)).to.be.false;
expect(await twilio.validateRequest(request, requestBody)).to.be.false;
});

// Twilio signature will always use the workspace public URL, which may difer from the request URL in some circumstances.
it('should use siteURL instead of request.url hostname', async () => {
process.env.TEST_MODE = 'false';

settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test');
settingsStub.get.withArgs('Site_Url').returns('https://ngrok.barn.com');

const twilio = new Twilio();
const requestBody = {
To: 'test',
From: 'test',
Body: 'test',
};

const request = {
url: 'https://example.com/api/v1/livechat/sms-incoming/twilio',
headers: {
get: (param: string) => {
const headers: Record<string, any> = {
'x-twilio-signature': getSignature('test', 'https://ngrok.barn.com/api/v1/livechat/sms-incoming/twilio', requestBody),
};

return headers[param];
},
},
};

expect(await twilio.validateRequest(request, requestBody)).to.be.true;
});
});
2 changes: 1 addition & 1 deletion packages/core-typings/src/omnichannel/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface ISMSProviderConstructor {

export interface ISMSProvider {
parse(data: unknown): ServiceData;
validateRequest(request: Request): Promise<boolean>;
validateRequest(request: Request, requestBody: unknown): Promise<boolean>;

sendBatch?(from: string, to: string[], message: string): Promise<SMSProviderResult>;
response(): SMSProviderResponse;
Expand Down
Loading