Skip to content

Instantly share code, notes, and snippets.

@kiler129
Last active January 25, 2023 06:29
Show Gist options
  • Save kiler129/ca2d8a617388fa302aeb91aaf5c94e6e to your computer and use it in GitHub Desktop.
Save kiler129/ca2d8a617388fa302aeb91aaf5c94e6e to your computer and use it in GitHub Desktop.
CloudFlare worker code forwards text messages from Twilio to your Telegram chat
/**
* This CloudFlare worker code forwards text messages from Twilio to your Telegram chat.
*
* You need to define the following variables in CloudFlare:
* ACCESS_USER - any user
* ACCESS_PASS - any password
* TELEGRAM_API_TOKEN - get from @BotFather
* TELEGRAM_CHAT_ID - get from https://api.telegram.org/bot<TELEGRAM_API_TOKEN>/getUpdates after messaging the bot
*
* Then set the following URL in Twilio as a POST WebHook:
* https://<ACCESS_USER>:<ACCESS_PASS>@name-of-your-worker.workers.dev/notify
*
* Note: this code does not verify Twilio signatures. However, this is safe as long as you don't leak ACCESS_USER and
* ACCESS_PASSS. The verification isn't implemented as the Twilio SDK doesn't work in workers and I didn't have
* time to manually debug the verification ;)
*
* Credits: this code is partially based on https://developers.cloudflare.com/workers/examples/basic-auth/
*/
const DEFAULT_HEADERS = {
'Content-Type': 'application/xml',
'Cache-Control': 'no-store',
};
async function handleRequest(request) {
const {protocol, pathname} = new URL(request.url);
if ('https:' !== protocol || 'https' !== request.headers.get('x-forwarded-proto')) {
throw new RejectRequest(400, 'Not HTTPS');
}
if (pathname !== '/notify') {
throw new RejectRequest(400, 'Not /notify');
}
if (!request.headers.has('Authorization')) {
//This must happen on production as per flow docks: https://www.twilio.com/docs/usage/security#http-authentication
//Without first 401-ing it will pass sandbox testing but it will fail on production!
throw new RejectRequest(401, 'No Authorization header (yet?)', {'WWW-Authenticate': 'Basic realm="worker"'});
}
const {user, pass} = basicAuthentication(request);
if (user !== ACCESS_USER || pass !== ACCESS_PASS) {
throw new RejectRequest(403, 'Wrong credentials received');
}
const contentType = request.headers.get('content-type') || '';
if (!contentType.includes('application/x-www-form-urlencoded')) {
throw new RejectRequest(400, 'Expected x-www-form-urlencoded');
}
const formData = await request.formData();
const body = {};
for (const entry of formData.entries()) {
body[entry[0]] = entry[1];
}
if (!body.hasOwnProperty('From') || !body.hasOwnProperty('Body')) {
console.error(body);
throw new RejectRequest(400, 'Expected to get "From" and "Body" - one of them was missing?!');
}
await sendTelegramMessage(body['From'], body['Body']);
return new Response('<Response></Response>', { status: 200, headers: DEFAULT_HEADERS });
}
async function sendTelegramMessage(from, contents) {
const tMessage = {
chat_id: TELEGRAM_CHAT_ID,
text: '<b>From:</b> ' + from + '\n<b>Contents:</b>\n' + contents,
parse_mode: 'HTML',
no_webpage: true,
noforwards: true,
};
const tReq = new Request(
'https://api.telegram.org/bot' + TELEGRAM_API_TOKEN + '/sendMessage',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tMessage),
}
);
return fetch(tReq);
}
function RejectRequest(code, reason, extraHeaders) {
this.status = code;
this.reason = reason;
this.extraHeaders = extraHeaders;
}
function basicAuthentication(request) {
const Authorization = request.headers.get('Authorization');
const [scheme, encoded] = Authorization.split(' ');
if (!encoded || scheme !== 'Basic') {
throw new RejectRequest(400, 'Invalid encoding or scheme != Basic');
}
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0));
const decoded = new TextDecoder().decode(buffer).normalize();
const index = decoded.indexOf(':');
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
throw new RejectRequest(400, 'Malformed authorization value');
}
return {user: decoded.substring(0, index), pass: decoded.substring(index + 1)};
}
addEventListener('fetch', event => {
event.respondWith(
handleRequest(event.request).catch(err => {
console.error('handleRequest reject: ' + err.reason);
return new Response('', {
status: err.status || 500,
statusText: null,
headers: (err.extraHeaders) ? {...DEFAULT_HEADERS, ...err.extraHeaders} : DEFAULT_HEADERS,
});
})
)
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment