Last active
January 25, 2023 06:29
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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