125 lines
3.6 KiB
JavaScript
125 lines
3.6 KiB
JavaScript
import { BODY_LIMIT, MODEL_UPLOAD_LIMIT } from './config.mjs'
|
|
|
|
export function setCorsHeaders(response) {
|
|
response.setHeader('Access-Control-Allow-Origin', process.env.CORS_ORIGIN || '*')
|
|
response.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
|
|
response.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
|
|
}
|
|
|
|
export function assertLocalDiagnosticsRequest(request) {
|
|
const remoteAddress = normalizeAddress(request.socket?.remoteAddress)
|
|
const origin = request.headers.origin
|
|
const referer = request.headers.referer
|
|
|
|
if (!isLocalHost(remoteAddress)) {
|
|
throw Object.assign(new Error('Diagnostics logs are only available from this machine.'), { status: 403 })
|
|
}
|
|
|
|
if (origin && !isLocalUrl(origin)) {
|
|
throw Object.assign(new Error('Diagnostics logs are only available to localhost pages.'), { status: 403 })
|
|
}
|
|
|
|
if (!origin && referer && !isLocalUrl(referer)) {
|
|
throw Object.assign(new Error('Diagnostics logs are only available to localhost pages.'), { status: 403 })
|
|
}
|
|
}
|
|
|
|
export function sendJson(response, status, payload) {
|
|
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
|
|
response.end(JSON.stringify(payload))
|
|
}
|
|
|
|
export function readJsonBody(request) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = []
|
|
let size = 0
|
|
|
|
request.on('data', (chunk) => {
|
|
size += chunk.length
|
|
if (size > BODY_LIMIT) {
|
|
reject(Object.assign(new Error('Image payload is too large.'), { status: 413 }))
|
|
request.destroy()
|
|
return
|
|
}
|
|
chunks.push(chunk)
|
|
})
|
|
|
|
request.on('end', () => {
|
|
try {
|
|
const raw = Buffer.concat(chunks).toString('utf8')
|
|
resolve(raw ? JSON.parse(raw) : {})
|
|
} catch {
|
|
reject(Object.assign(new Error('Invalid JSON payload.'), { status: 400 }))
|
|
}
|
|
})
|
|
|
|
request.on('error', reject)
|
|
})
|
|
}
|
|
|
|
export function readRawBody(request, limit = MODEL_UPLOAD_LIMIT) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = []
|
|
let size = 0
|
|
|
|
request.on('data', (chunk) => {
|
|
size += chunk.length
|
|
if (size > limit) {
|
|
reject(Object.assign(new Error('Model payload is too large.'), { status: 413 }))
|
|
request.destroy()
|
|
return
|
|
}
|
|
chunks.push(chunk)
|
|
})
|
|
|
|
request.on('end', () => {
|
|
resolve(Buffer.concat(chunks))
|
|
})
|
|
|
|
request.on('error', reject)
|
|
})
|
|
}
|
|
|
|
export function parseDataUrl(dataUrl) {
|
|
if (typeof dataUrl !== 'string') {
|
|
throw Object.assign(new Error('imageDataUrl is required.'), { status: 400 })
|
|
}
|
|
|
|
const match = dataUrl.match(/^data:(image\/(?:png|jpe?g|webp));base64,(.+)$/)
|
|
if (!match) {
|
|
throw Object.assign(new Error('Only PNG, JPEG, or WebP image data URLs are supported.'), { status: 400 })
|
|
}
|
|
|
|
const mime = match[1]
|
|
const buffer = Buffer.from(match[2], 'base64')
|
|
const ext = mime.includes('png') ? 'png' : mime.includes('webp') ? 'webp' : 'jpg'
|
|
|
|
if (buffer.length < 1024) {
|
|
throw Object.assign(new Error('Image is too small for 3D generation.'), { status: 400 })
|
|
}
|
|
|
|
return { mime, buffer, ext }
|
|
}
|
|
|
|
export function sanitizeFileName(fileName) {
|
|
const baseName = String(fileName).split(/[\\/]/).pop() || ''
|
|
return baseName.replace(/[^\w.\- ]+/g, '').replace(/^\.+/, '').trim() || 'asset-reference.png'
|
|
}
|
|
|
|
function normalizeAddress(address = '') {
|
|
return String(address).replace(/^::ffff:/, '')
|
|
}
|
|
|
|
function isLocalHost(hostname = '') {
|
|
const normalized = String(hostname).toLowerCase()
|
|
return normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1'
|
|
}
|
|
|
|
function isLocalUrl(value) {
|
|
try {
|
|
return isLocalHost(new URL(value).hostname)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|