feat: better size check for url uploads

first layer is via sending HEAD request to the url to determine its size
via content-length header
however not all hosts properly set the header, so we ignore it if
it isn't a valid number

next via size option in fetch(), which supposedly limits response body
size during the request itself (?)

lastly via checking actual bytes written to physical file as reported by
fs.createWriteStream()
This commit is contained in:
Bobby 2022-05-08 15:19:29 +07:00
parent 71a6adc3d3
commit ce71a9e8d6
No known key found for this signature in database
GPG Key ID: 941839794CBF5A09

View File

@ -30,7 +30,8 @@ const fileIdentifierLengthChangeable = !config.uploads.fileIdentifierLength.forc
const maxSize = parseInt(config.uploads.maxSize)
const maxSizeBytes = maxSize * 1e6
const urlMaxSizeBytes = parseInt(config.uploads.urlMaxSize) * 1e6
const urlMaxSize = parseInt(config.uploads.urlMaxSize)
const urlMaxSizeBytes = urlMaxSize * 1e6
const maxFilesPerUpload = 20
@ -382,6 +383,18 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
throw new ClientError(`Maximum ${maxFilesPerUpload} URLs at a time.`)
}
const assertSize = (size, isContentLength = false) => {
if (config.filterEmptyFile && size === 0) {
throw new ClientError('Empty files are not allowed.')
} else if (size > urlMaxSizeBytes) {
if (isContentLength) {
throw new ClientError(`File too large. Content-Length header reports file is bigger than ${urlMaxSize} MB.`)
} else {
throw new ClientError(`File too large. File is bigger than ${urlMaxSize} MB.`)
}
}
}
const downloaded = []
const infoMap = []
try {
@ -419,8 +432,25 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
// Push to array early, so regardless of its progress it will be deleted on errors
downloaded.push(destination)
// Try to determine size early via Content-Length header,
// but continue anyway if it isn't a valid number
try {
const head = await fetch(url, { method: 'HEAD', size: urlMaxSizeBytes })
if (head.status === 200) {
const contentLength = parseInt(head.headers.get('content-length'))
if (!Number.isNaN(contentLength)) {
assertSize(contentLength, true)
}
}
} catch (ex) {
// Re-throw only if ClientError, otherwise ignore
if (ex instanceof ClientError) {
throw ex
}
}
// Limit max response body size with maximum allowed size
const fetchFile = await fetch(url, { size: urlMaxSizeBytes })
const fetchFile = await fetch(url, { method: 'GET', size: urlMaxSizeBytes })
.then(res => new Promise((resolve, reject) => {
if (res.status === 200) {
const onerror = error => {
@ -443,6 +473,8 @@ self.actuallyUploadUrls = async (req, res, user, albumid, age) => {
}
const contentType = fetchFile.headers.get('content-type')
// Re-test size via actual bytes written to physical file
assertSize(outStream.bytesWritten)
infoMap.push({
path: destination,