Skip to content

Commit

Permalink
Improve URL handling
Browse files Browse the repository at this point in the history
* Ensure all URLs are http or https
* Ensure all URLs do not have duplicate `/` characters anywhere (might cause issues with some web servers)
* Replace URL strings with `URL` objects to prevent most malformations

Fixes #7
  • Loading branch information
Dani Llewellyn committed Nov 3, 2022
1 parent 48dd900 commit fd943cc
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 17 deletions.
33 changes: 23 additions & 10 deletions api/mastodon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ function getMastodonApp(mastodonDomain) {
* @returns Promise<string>
*/
async function createMastodonApp(mastodonHost, mastodonDomain, redirectUri) {
const response = await fetch(`${mastodonHost}/api/v1/apps`, {
const url = new URL('/api/v1/apps', mastodonHost)
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down Expand Up @@ -65,11 +66,21 @@ async function getOrCreateMastodonApp(mastodonHost, mastodonDomain, redirectUri)
* @returns void
*/
export async function mastodonLoginUrl(request, reply) {
const mastodonHost = new URL(request.query.mastodonHost)
const mastodonDomain = (new URL(mastodonHost)).hostname
const redirectUri = new URL(`${request.protocol}://${request.hostname}/mastodonAuth`)
let tempMastodonHost = request.query.mastodonHost
if (!/^https?:\/\//.test(tempMastodonHost)) {
tempMastodonHost = `https://${tempMastodonHost.replace(/^.*:([/]{2})?/, '')}`
}
const mastodonHost = new URL(tempMastodonHost)
mastodonHost.pathname = ''
const redirectUri = new URL('/mastodonAuth', `${request.protocol}://${request.hostname}/`)

const client_id = await getOrCreateMastodonApp.call(this, mastodonHost.href, mastodonHost.hostname, redirectUri.href)

const client_id = await getOrCreateMastodonApp.call(this, mastodonHost.href, mastodonDomain, redirectUri.href)
const mastodonLoginUrl = new URL('/oauth/authorize', mastodonHost.href)
mastodonLoginUrl.searchParams.set('response_type', 'code')
mastodonLoginUrl.searchParams.set('client_id', client_id)
mastodonLoginUrl.searchParams.set('redirect_uri', redirectUri.href)
mastodonLoginUrl.searchParams.set('scope', scopes)

reply
.setCookie(mastodonHostCookieName, mastodonHost.href, {
Expand All @@ -78,7 +89,7 @@ export async function mastodonLoginUrl(request, reply) {
sameSite: 'lax',
})
.send({
mastodonLoginUrl: `${mastodonHost.href}/oauth/authorize?response_type=code&client_id=${client_id}&redirect_uri=${encodeURIComponent(redirectUri.href)}&scope=${scopes}`
mastodonLoginUrl: mastodonLoginUrl.href,
})
}

Expand All @@ -94,7 +105,7 @@ export async function mastodonAuth(request, reply) {
}

const mastodonDomain = (new URL(mastodonHost.value)).hostname
const redirectUri = new URL(`${request.protocol}://${request.hostname}/mastodonAuth`)
const redirectUri = new URL('/mastodonAuth', `${request.protocol}://${request.hostname}/`)

const {client_id, client_secret} = await getMastodonApp.call(this, mastodonDomain)

Expand All @@ -109,7 +120,8 @@ export async function mastodonAuth(request, reply) {
`&redirect_uri=${encodeURIComponent(redirectUri.href)}` +
`&scope=${encodeURIComponent(scopes)}`

const oauthData = await fetch(`${mastodonHost.value}/oauth/token`, {
const url = new URL('/oauth/token', mastodonHost.value)
const oauthData = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand Down Expand Up @@ -158,7 +170,8 @@ export async function mastodonDeAuth(request, reply) {
`&client_secret=${encodeURIComponent(client_secret)}` +
`&token=${encodeURIComponent(tokenCookie.value)}`

const res = await fetch(`${hostCookie.value}/oauth/revoke`, {
const url = new URL('/oauth/revoke', hostCookie.value)
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Expand All @@ -174,7 +187,7 @@ export async function mastodonDeAuth(request, reply) {
}

export async function meHandler(host, token) {
const url = `${host}/api/v1/accounts/verify_credentials`
const url = new URL('/api/v1/accounts/verify_credentials', host)
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
Expand Down
26 changes: 19 additions & 7 deletions api/twitter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,25 @@ const client_id = process.env.TWITTER_CLIENT_ID
export async function twitterLoginUrl(request, reply) {
const id = randomBytes(32).toString('hex')
const challenge = randomBytes(32).toString('hex')
const redirectUri = new URL(`${request.protocol}://${request.hostname}/twitterAuth`)
const redirectUri = new URL('/twitterAuth', `${request.protocol}://${request.hostname}/`)

await this.mongo.db.collection('twitter_user_tokens').insertOne({
_id: id,
challenge,
CreatedDate: new Date(),
})

const twitterLoginUrl = new URL('/i/oauth2/authorize', 'https://twitter.com/')
twitterLoginUrl.searchParams.set('response_type', 'code')
twitterLoginUrl.searchParams.set('client_id', client_id)
twitterLoginUrl.searchParams.set('redirect_uri', redirectUri.href)
twitterLoginUrl.searchParams.set('state', id)
twitterLoginUrl.searchParams.set('scope', 'tweet.read users.read follows.read')
twitterLoginUrl.searchParams.set('code_challenge', challenge)
twitterLoginUrl.searchParams.set('code_challenge_method', 'plain')

reply.send({
twitterLoginUrl: `https://twitter.com/i/oauth2/authorize?response_type=code&client_id=${client_id}&redirect_uri=${encodeURIComponent(redirectUri.href)}&scope=tweet.read%20users.read%20follows.read&state=${id}&code_challenge=${challenge}&code_challenge_method=plain`
twitterLoginUrl: twitterLoginUrl.href
})
}

Expand All @@ -34,7 +43,7 @@ export async function twitterLoginUrl(request, reply) {
export async function twitterAuth(request, reply) {
const id = request.query.state
const {challenge} = await this.mongo.db.collection('twitter_user_tokens').findOne({ _id: id })
const redirectUri = new URL(`${request.protocol}://${request.hostname}/twitterAuth`)
const redirectUri = new URL('/twitterAuth', `${request.protocol}://${request.hostname}/`)

const body = `code=${encodeURIComponent(request.query.code)}` +
'&grant_type=authorization_code' +
Expand All @@ -54,7 +63,6 @@ export async function twitterAuth(request, reply) {

if (oauthData.status !== 200) {
return reply.send(json)
return reply.code(oauthData.status).clearCookie(twitterTokenCookieName).send()
}

if (!'expires_in' in json || !json.expires_in || json.expires_in <= 0) {
Expand Down Expand Up @@ -144,8 +152,12 @@ export async function followingOnTwitter(request, reply) {
let nextToken = '';
const users = []
while (true) {
const response = await fetch(
`https://api.twitter.com/2/users/${userId}/following?max_results=1000${nextToken}`,
const url = new URL(`/2/users/${userId}/following`, 'https://api.twitter.com/')
url.searchParams.set('max_results', 1000)
if (nextToken) {
url.searchParams.set('pagination_token', nextToken)
}
const response = await fetch(url,
{ headers: { Authorization: `Bearer ${token.value}` } }
)
if (!response.ok) {
Expand All @@ -164,7 +176,7 @@ export async function followingOnTwitter(request, reply) {
if (!json.meta.next_token) {
break
}
nextToken = `&pagination_token=${json.meta.next_token}`
nextToken = json.meta.next_token
}

reply.send(users.flat(1))
Expand Down

0 comments on commit fd943cc

Please sign in to comment.