Skip to content

Instantly share code, notes, and snippets.

@Tymek
Created August 21, 2024 17:20
Show Gist options
  • Save Tymek/b3a84dff351903879852761024b50f19 to your computer and use it in GitHub Desktop.
Save Tymek/b3a84dff351903879852761024b50f19 to your computer and use it in GitHub Desktop.
ts image size handler
import http from 'node:http'
import https from 'node:https'
import sizeOf from 'image-size'
const MAX_REDIRECTS = Number.parseInt(
process.env.IMGMETA_MAX_REDIRECTS || '10',
10,
)
const ALLOWED_DOMAINS = process.env.IMGMETA_ALLOWED_DOMAINS || '*'
const TIMEOUT =
Number.parseInt(process.env.IMGMETA_FETCH_TIMEOUT || '10', 10) * 1000
const _getPathname = (url: string): string => {
const pathname = new URL(url).pathname
return pathname.startsWith('/') ? pathname.slice(1) : pathname
}
const _parseUrl = (pathname: string): string | null => {
if (pathname === '') {
return null
}
try {
const path =
pathname.startsWith('http://') || pathname.startsWith('https://')
? pathname
: atob(pathname)
try {
const url = new URL(path)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return null
}
return url.href
} catch {
return null
}
} catch {
return null
}
}
const _isDomainAllowed = (imageUrl: string): boolean => {
if (ALLOWED_DOMAINS === '*') {
return true
}
const allowedDomains = process.env.IMGMETA_ALLOWED_DOMAINS?.split(',') || []
return allowedDomains.some(
allowed =>
imageUrl.startsWith(allowed) ||
imageUrl.startsWith(`https://${allowed}`) ||
imageUrl.startsWith(`http://${allowed}`),
)
}
const _getImage = async (
url: string,
redirects = MAX_REDIRECTS,
): Promise<Uint8Array> =>
new Promise((resolve, reject) => {
const getter = url.startsWith('https') ? https.get : http.get
getter(url, { timeout: TIMEOUT }, res => {
const { statusCode } = res
if (statusCode && statusCode >= 300 && statusCode < 400) {
if (redirects <= 0) {
reject(new Error('Too many redirects'))
res.resume()
return
}
const location = res.headers.location
if (!location) {
reject(new Error('No location header'))
res.resume()
return
}
return resolve(_getImage(location, redirects - 1))
}
let data: Uint8Array = new Uint8Array()
const limit = 512 // kb
let size = 0
res.on('data', (chunk: Uint8Array) => {
size += chunk.length
if (size > limit * 1024) {
return resolve(data)
}
data = new Uint8Array([...data, ...chunk])
})
res.on('end', () => {
resolve(data)
})
res.on('error', reject)
})
})
export const handler = async (req: Request): Promise<Response> => {
try {
const pathname = _getPathname(req.url)
if (pathname === 'healthcheck') {
return new Response('OK')
}
const imageUrl = _parseUrl(pathname)
if (!imageUrl) {
return new Response(`Invalid URL: ${req.url}`, { status: 400 })
}
if (!_isDomainAllowed(imageUrl)) {
return new Response(`Domain not allowed: ${req.url}`, { status: 403 })
}
const imageResponse = await _getImage(imageUrl)
if (!imageResponse || imageResponse.length === 0) {
return new Response(`Fetch failed: ${req.url}`, { status: 400 })
}
const metadata = await sizeOf(imageResponse)
if (!metadata) {
return new Response(`Invalid image: ${req.url}`, { status: 415 })
}
return new Response(JSON.stringify(metadata), {
headers: { 'Content-Type': 'application/json' },
})
} catch {
return new Response(`Processing failed: ${req.url}`, { status: 500 })
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment