mirror of
https://github.com/BobbyWibowo/lolisafe.git
synced 2025-01-19 01:31:34 +00:00
Updates
* Added an experimental virus scanning feature using ClamAV. This has only been tested with an Ubuntu machine. * File extensions will now be parsed with path-complete-extname module. This will ensure extensions such as .tar.gz are properly parsed. Notice: It may take a minute or so to start the safe with virus scanning, as apparently the module takes a while to create the engine. I'm guessing since it'll be loaded to memory? Either way, once the engine is created, everything should work fine. Virus scanning should also not have that much of an impact to the upload time.
This commit is contained in:
parent
bf31a59f2c
commit
36da76357e
10
README.md
10
README.md
@ -29,6 +29,16 @@ force: 0 = no force (default), 1 = overwrite existing thumbnails
|
||||
|
||||
For example, if you only want to generate thumbnails for image files, you can do `yarn thumbs 1`.
|
||||
|
||||
## Virus scanning
|
||||
|
||||
This project is using [clamav](https://www.clamav.net/) to scan files for viruses through [node-clam-engine](https://github.com/srijs/node-clam-engine).
|
||||
|
||||
You will need to install dependencies listed [here](https://github.com/srijs/node-clam-engine#installation).
|
||||
|
||||
If you do not want to use it, you may remove `clam-engine` from the `package.json` file.
|
||||
|
||||
This has only been tested with an Ubuntu machine.
|
||||
|
||||
## Running
|
||||
|
||||
1. Ensure you have at least version 8.0.0 of node installed
|
||||
|
@ -92,6 +92,11 @@ module.exports = {
|
||||
*/
|
||||
urlMaxSize: '32MB',
|
||||
|
||||
/*
|
||||
Scan for virus using ClamAV.
|
||||
*/
|
||||
scan: false,
|
||||
|
||||
/*
|
||||
Use DuckDuckGo's proxy when fetching any URL uploads.
|
||||
This may be considered a hack and not supported by DuckDuckGo, so USE AT YOUR OWN RISK.
|
||||
|
@ -3,6 +3,7 @@ const db = require('knex')(config.database)
|
||||
const EventEmitter = require('events')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const pce = require('path-complete-extname')
|
||||
const randomstring = require('randomstring')
|
||||
const utils = require('./utilsController')
|
||||
const Zip = require('jszip')
|
||||
@ -298,7 +299,7 @@ albumsController.get = async (req, res, next) => {
|
||||
for (const file of files) {
|
||||
file.file = `${config.domain}/${file.name}`
|
||||
|
||||
const extname = path.extname(file.name).toLowerCase()
|
||||
const extname = pce(file.name).toLowerCase()
|
||||
if (utils.mayGenerateThumb(extname)) {
|
||||
file.thumb = `${config.domain}/thumbs/${file.name.slice(0, -extname.length)}.png`
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
const config = require('./../config')
|
||||
const path = require('path')
|
||||
const multer = require('multer')
|
||||
const randomstring = require('randomstring')
|
||||
const db = require('knex')(config.database)
|
||||
const crypto = require('crypto')
|
||||
const db = require('knex')(config.database)
|
||||
const fs = require('fs')
|
||||
const multer = require('multer')
|
||||
const path = require('path')
|
||||
const pce = require('path-complete-extname')
|
||||
const randomstring = require('randomstring')
|
||||
const snekfetch = require('snekfetch')
|
||||
const utils = require('./utilsController')
|
||||
|
||||
@ -39,7 +40,7 @@ const storage = multer.diskStorage({
|
||||
filename (req, file, cb) {
|
||||
// If chunked uploads is disabled or the uploaded file is not a chunk
|
||||
if (!chunkedUploads || (req.body.uuid === undefined && req.body.chunkindex === undefined)) {
|
||||
const extension = path.extname(file.originalname)
|
||||
const extension = pce(file.originalname).toLowerCase()
|
||||
const length = uploadsController.getFileNameLength(req)
|
||||
return uploadsController.getUniqueRandomName(length, extension)
|
||||
.then(name => cb(null, name))
|
||||
@ -60,7 +61,7 @@ const upload = multer({
|
||||
fileSize: maxSizeBytes
|
||||
},
|
||||
fileFilter (req, file, cb) {
|
||||
const extname = path.extname(file.originalname).toLowerCase()
|
||||
const extname = pce(file.originalname).toLowerCase()
|
||||
if (uploadsController.isExtensionFiltered(extname)) {
|
||||
// eslint-disable-next-line standard/no-callback-literal
|
||||
return cb(`${extname.substr(1).toUpperCase()} files are not permitted due to security reasons.`)
|
||||
@ -178,6 +179,11 @@ uploadsController.actuallyUpload = async (req, res, user, albumid) => {
|
||||
}
|
||||
})
|
||||
|
||||
if (config.uploads.scan) {
|
||||
const scan = await uploadsController.scanFiles(req, infoMap)
|
||||
if (!scan) { return erred('Virus detected.') }
|
||||
}
|
||||
|
||||
const result = await uploadsController.formatInfoMap(req, res, user, infoMap)
|
||||
.catch(erred)
|
||||
if (!result) { return }
|
||||
@ -210,7 +216,7 @@ uploadsController.actuallyUploadByUrl = async (req, res, user, albumid) => {
|
||||
const infoMap = []
|
||||
for (const url of urls) {
|
||||
const original = path.basename(url).split(/[?#]/)[0]
|
||||
const extension = path.extname(original)
|
||||
const extension = pce(original).toLowerCase()
|
||||
if (uploadsController.isExtensionFiltered(extension)) {
|
||||
return erred(`${extension.substr(1).toUpperCase()} files are not permitted due to security reasons.`)
|
||||
}
|
||||
@ -257,6 +263,11 @@ uploadsController.actuallyUploadByUrl = async (req, res, user, albumid) => {
|
||||
|
||||
iteration++
|
||||
if (iteration === urls.length) {
|
||||
if (config.uploads.scan) {
|
||||
const scan = await uploadsController.scanFiles(req, infoMap)
|
||||
if (!scan) { return erred('Virus detected.') }
|
||||
}
|
||||
|
||||
const result = await uploadsController.formatInfoMap(req, res, user, infoMap)
|
||||
.catch(erred)
|
||||
if (!result) { return }
|
||||
@ -320,7 +331,7 @@ uploadsController.actuallyFinishChunks = async (req, res, user, albumid) => {
|
||||
if (error) { return erred(error) }
|
||||
if (file.count < chunkNames.length) { return erred('Chunks count mismatch.') }
|
||||
|
||||
const extension = typeof file.original === 'string' ? path.extname(file.original) : ''
|
||||
const extension = typeof file.original === 'string' ? pce(file.original).toLowerCase() : ''
|
||||
if (uploadsController.isExtensionFiltered(extension)) {
|
||||
return erred(`${extension.substr(1).toUpperCase()} files are not permitted due to security reasons.`)
|
||||
}
|
||||
@ -375,6 +386,11 @@ uploadsController.actuallyFinishChunks = async (req, res, user, albumid) => {
|
||||
|
||||
iteration++
|
||||
if (iteration === files.length) {
|
||||
if (config.uploads.scan) {
|
||||
const scan = await uploadsController.scanFiles(req, infoMap)
|
||||
if (!scan) { return erred('Virus detected.') }
|
||||
}
|
||||
|
||||
const result = await uploadsController.formatInfoMap(req, res, user, infoMap)
|
||||
.catch(erred)
|
||||
if (!result) { return }
|
||||
@ -444,7 +460,7 @@ uploadsController.cleanUpChunks = (uuidDir, chunkNames) => {
|
||||
}
|
||||
|
||||
uploadsController.formatInfoMap = (req, res, user, infoMap) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async resolve => {
|
||||
let iteration = 0
|
||||
const files = []
|
||||
const existingFiles = []
|
||||
@ -511,6 +527,40 @@ uploadsController.formatInfoMap = (req, res, user, infoMap) => {
|
||||
})
|
||||
}
|
||||
|
||||
uploadsController.scanFiles = async (req, infoMap) => {
|
||||
const dirty = await new Promise(async resolve => {
|
||||
let iteration = 0
|
||||
const engine = req.app.get('clam-engine')
|
||||
const dirty = []
|
||||
|
||||
for (const info of infoMap) {
|
||||
const log = message => {
|
||||
console.log(`ClamAV: ${info.data.filename}: ${message}.`)
|
||||
}
|
||||
|
||||
engine.scanFile(info.path, (error, virus) => {
|
||||
if (error || virus) {
|
||||
if (error) { log(error.toString()) }
|
||||
if (virus) { log(`${virus} detected`) }
|
||||
dirty.push(info)
|
||||
} else {
|
||||
// log('OK')
|
||||
}
|
||||
|
||||
iteration++
|
||||
if (iteration === infoMap.length) {
|
||||
resolve(dirty)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!dirty.length) { return true }
|
||||
|
||||
// If there is at least one dirty file, delete all files
|
||||
infoMap.forEach(info => utils.deleteFile(info.data.filename).catch(console.error))
|
||||
}
|
||||
|
||||
uploadsController.processFilesForDisplay = async (req, res, files, existingFiles) => {
|
||||
const responseFiles = []
|
||||
|
||||
@ -546,7 +596,7 @@ uploadsController.processFilesForDisplay = async (req, res, files, existingFiles
|
||||
if (file.albumid && !albumids.includes(file.albumid)) {
|
||||
albumids.push(file.albumid)
|
||||
}
|
||||
if (utils.mayGenerateThumb(path.extname(file.name).toLowerCase())) {
|
||||
if (utils.mayGenerateThumb(pce(file.name).toLowerCase())) {
|
||||
utils.generateThumbs(file.name)
|
||||
}
|
||||
}
|
||||
@ -639,7 +689,7 @@ uploadsController.list = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
file.extname = path.extname(file.name).toLowerCase()
|
||||
file.extname = pce(file.name).toLowerCase()
|
||||
if (utils.mayGenerateThumb(file.extname)) {
|
||||
file.thumb = `${basedomain}/thumbs/${file.name.slice(0, -file.extname.length)}.png`
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ const ffmpeg = require('fluent-ffmpeg')
|
||||
const fs = require('fs')
|
||||
const gm = require('gm')
|
||||
const path = require('path')
|
||||
const pce = require('path-complete-extname')
|
||||
const snekfetch = require('snekfetch')
|
||||
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
@ -65,7 +66,7 @@ utilsController.authorize = async (req, res) => {
|
||||
|
||||
utilsController.generateThumbs = (name, force) => {
|
||||
return new Promise(resolve => {
|
||||
const extname = path.extname(name).toLowerCase()
|
||||
const extname = pce(name).toLowerCase()
|
||||
const thumbname = path.join(thumbsDir, name.slice(0, -extname.length) + '.png')
|
||||
fs.access(thumbname, error => {
|
||||
if (error && error.code !== 'ENOENT') {
|
||||
@ -118,7 +119,7 @@ utilsController.generateThumbs = (name, force) => {
|
||||
|
||||
utilsController.deleteFile = file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const extname = path.extname(file).toLowerCase()
|
||||
const extname = pce(file).toLowerCase()
|
||||
return fs.unlink(path.join(uploadsDir, file), error => {
|
||||
if (error && error.code !== 'ENOENT') { return reject(error) }
|
||||
|
||||
@ -212,7 +213,7 @@ utilsController.purgeCloudflareCache = async names => {
|
||||
const thumbs = []
|
||||
names = names.map(name => {
|
||||
const url = `${config.domain}/${name}`
|
||||
const extname = path.extname(name).toLowerCase()
|
||||
const extname = pce(name).toLowerCase()
|
||||
if (utilsController.mayGenerateThumb(extname)) {
|
||||
thumbs.push(`${config.domain}/thumbs/${name.slice(0, -extname.length)}.png`)
|
||||
}
|
||||
|
28
lolisafe.js
28
lolisafe.js
@ -84,8 +84,6 @@ safe.use((error, req, res, next) => {
|
||||
res.status(500).sendFile('500.html', { root: './pages/error/' })
|
||||
})
|
||||
|
||||
safe.listen(config.port, () => console.log(`lolisafe started on port ${config.port}`))
|
||||
|
||||
process.on('uncaughtException', error => {
|
||||
console.error('Uncaught Exception:')
|
||||
console.error(error)
|
||||
@ -95,3 +93,29 @@ process.on('unhandledRejection', error => {
|
||||
console.error('Unhandled Rejection (Promise):')
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
async function start () {
|
||||
if (config.uploads.scan) {
|
||||
// Placing require() here so the package does not have to exist when the option is not enabled
|
||||
const clam = require('clam-engine')
|
||||
const created = await new Promise(resolve => {
|
||||
process.stdout.write('Creating clam-engine...')
|
||||
clam.createEngine(function (error, engine) {
|
||||
if (error) {
|
||||
process.stdout.write(' ERROR\n')
|
||||
console.error(error)
|
||||
return resolve(false)
|
||||
}
|
||||
safe.set('clam-engine', engine)
|
||||
process.stdout.write(' OK\n')
|
||||
console.log(`ClamAV ${engine.version} (${engine.signatures} sigs)`)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
if (!created) { return }
|
||||
}
|
||||
|
||||
safe.listen(config.port, () => console.log(`lolisafe started on port ${config.port}`))
|
||||
}
|
||||
|
||||
start()
|
||||
|
@ -22,6 +22,7 @@
|
||||
"dependencies": {
|
||||
"bcrypt": "^2.0.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"clam-engine": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"express-rate-limit": "^2.11.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@ -31,6 +32,7 @@
|
||||
"knex": "^0.14.6",
|
||||
"multer": "^1.3.0",
|
||||
"nunjucks": "^3.1.2",
|
||||
"path-complete-extname": "^1.0.0",
|
||||
"randomstring": "^1.1.5",
|
||||
"snekfetch": "^3.6.4",
|
||||
"sqlite3": "^4.0.0"
|
||||
|
14
yarn.lock
14
yarn.lock
@ -360,6 +360,12 @@ circular-json@^0.3.1:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||
|
||||
clam-engine@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clam-engine/-/clam-engine-2.0.1.tgz#ad94dfa8e1d966d13e200dfd5d794f15425bccc5"
|
||||
dependencies:
|
||||
nan "^2.1.0"
|
||||
|
||||
class-utils@^0.3.5:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
|
||||
@ -1758,6 +1764,10 @@ nan@2.10.0, nan@^2.9.2:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
nan@^2.1.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.0.tgz#574e360e4d954ab16966ec102c0c049fd961a099"
|
||||
|
||||
nan@~2.9.2:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866"
|
||||
@ -2011,6 +2021,10 @@ pascalcase@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||
|
||||
path-complete-extname@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-1.0.0.tgz#f889985dc91000c815515c0bfed06c5acda0752b"
|
||||
|
||||
path-dirname@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
|
||||
|
Loading…
Reference in New Issue
Block a user