Skip to content

Instantly share code, notes, and snippets.

@kiler129
Created February 28, 2024 03:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kiler129/5d49ab331b91976e0b4da0d9461379aa to your computer and use it in GitHub Desktop.
Save kiler129/5d49ab331b91976e0b4da0d9461379aa to your computer and use it in GitHub Desktop.
CloudFlare worker to redirect SMS to Telegram
// Receives Twilio SMS WebHook and sends message via Telegram
//
//Define the following as variables on CF:
// - ACCESS_USER (encrypt): long unpredictible string, shared with Twilio
// - ACCESS_PASS (encrypt): long unpredictible string, shared with Twilio
// - TELEGRAM_CHAT_ID: chat id for Telegram conversation
// - TELEGRAM_API_TOKEN (encrypt): API token for Telegram intragration
//
// Configure Twilio with SMS WebHook to call https://<ACCESS_USER>:<ACCESS_PASS>@<worker name>.<cf login>.workers.dev/notify
// e.g. https://foo:bar@twilio-telegram-fwd.superuser123.workers.dev/notify
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');
}
//See https://www.twilio.com/docs/messaging/guides/webhook-request for details
const formData = await request.formData();
const body = {};
for (const entry of formData.entries()) {
body[entry[0]] = entry[1];
}
if (!body.hasOwnProperty('From') || !body.hasOwnProperty('To') || !body.hasOwnProperty('Body')) {
console.error(body);
throw new RejectRequest(400, 'Expected to get "From", "To", and "Body" - one of them was missing?!');
}
await sendTelegramMessage(body['From'], body['To'], body['Body']);
return new Response('<Response></Response>', { status: 200, headers: DEFAULT_HEADERS });
}
async function sendTelegramMessage(from, to, contents) {
const tMessage = {
chat_id: TELEGRAM_CHAT_ID,
text: '<b>From:</b> ' + from + '\n<b>To:</b> ' + to + '\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