Compare commits

..

No commits in common. "master" and "v2.2.3" have entirely different histories.

16 changed files with 139 additions and 772 deletions

3
.gitignore vendored
View file

@ -60,6 +60,9 @@ typings/
# Local development config file
config/*.json
# Public folder should be ignored
public/*
# lol
package-lock.json

View file

@ -39,21 +39,10 @@ export function uploadFile(ctx, siteName, noprefix = false) {
form.uploadDir = `${config.get('uploadFolder')}/${siteName}`
form.maxFileSize = config.get('fileSize')
let siteSize = config.get(`sites:${siteName}:fileSize`)
if (siteSize && typeof(siteSize) === 'number') {
form.maxFileSize = siteSize
}
form.parse(ctx.req, function(err, fields, files) {
if (err) return rej(err)
if (!files || !files.file) return rej(new HttpError(422, 'File in body was missing'))
let file = files.file
let filename = file.name.replace(/ /g, '_')
.replace(/&/g, 'and')
.replace(/'/g, '')
.replace(/"/g, '')
.replace(/\?/g, '')
Object.keys(fields).forEach(function(key) {
try {
@ -62,14 +51,14 @@ export function uploadFile(ctx, siteName, noprefix = false) {
})
ctx.req.body = fields
if (!noprefix || fs.existsSync(`${config.get('uploadFolder')}/${siteName}/${prefix}${filename}`)) {
if (!noprefix || fs.existsSync(`${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}`)) {
prefix = getPrefix()
}
fs.rename(files.file.path, `${config.get('uploadFolder')}/${siteName}/${prefix}${filename}`, function(err) {
fs.rename(files.file.path, `${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}`, function(err) {
if (err) return rej(err)
file.path = `${config.get('uploadFolder')}/${siteName}/${prefix}${filename}`
file.filename = `${prefix}${filename}`
file.path = `${config.get('uploadFolder')}/${siteName}/${prefix}${file.name}`
file.filename = `${prefix}${file.name}`
return res(file)
})

View file

@ -2,7 +2,7 @@ import path from 'path'
import sharp from 'sharp'
import fs from 'fs/promises'
import config from '../config.mjs'
import { HttpError, CorsHandler } from 'flaska'
import { HttpError } from 'flaska'
import * as security from './security.mjs'
import * as formidable from './formidable.mjs'
@ -16,11 +16,10 @@ export default class MediaRoutes {
})
this.siteCache = new Map()
this.collator = new Intl.Collator('is-IS', { numeric: false, sensitivity: 'accent' })
this.cors = CorsHandler({ allowedOrigins: ['*'] })
}
register(server) {
this.init(server).then(function() {}, function(err) {
this.init().then(function() {}, function(err) {
server.core.log.error(err, 'Error initing media')
})
@ -31,27 +30,26 @@ export default class MediaRoutes {
server.flaska.post('/media/resize', [server.queryHandler()], this.resize.bind(this))
server.flaska.post('/media/resize/:filename', [server.queryHandler(), server.jsonHandler()], this.resizeExisting.bind(this))
server.flaska.delete('/media/:filename', [server.queryHandler()], this.remove.bind(this))
server.flaska.options('/::path', [server.queryHandler(), this.security.verifyCorsEnabled], this.cors)
}
init(server) {
let folders = Object.keys(config.get('sites'))
return Promise.all(folders.map(folder => {
return fs.readdir(config.get('uploadFolder') + '/' + folder)
.then(files => {
return files.map(file => {
return { filename: file, size: null }
init() {
return fs.readdir(config.get('uploadFolder')).then(folders => {
return Promise.all(folders.map(folder => {
return fs.readdir(config.get('uploadFolder') + '/' + folder)
.then(files => {
return Promise.all(files.map(file => {
return fs.stat(`${config.get('uploadFolder')}/${folder}/${file}`)
.then(function(stat) {
return { filename: file, size: stat.size }
})
}))
})
})
.catch(err => {
server.core.log.error(err, `Error reading folder "${config.get('uploadFolder')}/${folder}"`)
return []
})
.then(files => {
this.siteCache.set(folder, files)
})
}))
.then(files => {
this.siteCache.set(folder, files)
})
.catch(function() {})
}))
})
}
filesCacheGet(site) {
@ -91,32 +89,20 @@ export default class MediaRoutes {
}
}
getSite(ctx) {
let site = this.security.verifyToken(ctx)
if (this.security.hasCors(site)) {
this.cors(ctx)
}
return site
}
async listFiles(ctx) {
let site = this.getSite(ctx)
let site = await this.security.verifyToken(ctx)
ctx.body = this.filesCacheGet(site)
}
async listPublicFiles(ctx) {
this.security.throwIfNotPublic(ctx.params.site)
if (this.security.hasCors(ctx.params.site)) {
this.cors(ctx)
}
ctx.body = this.filesCacheGet(ctx.params.site)
}
async upload(ctx, noprefix = false) {
let site = this.getSite(ctx)
let site = await this.security.verifyToken(ctx)
ctx.state.site = site
let result = await this.formidable.uploadFile(ctx, ctx.state.site, noprefix)
@ -149,7 +135,7 @@ export default class MediaRoutes {
'extend',
]
await Promise.all(keys.filter(key => ctx.req.body[key]).map(key => {
await Promise.all(keys.map(key => {
return Promise.resolve()
.then(async () => {
let item = ctx.req.body[key]
@ -195,7 +181,7 @@ export default class MediaRoutes {
}
async resizeExisting(ctx) {
let site = this.getSite(ctx)
let site = await this.security.verifyToken(ctx)
ctx.state.site = site
ctx.body = {}
@ -209,20 +195,13 @@ export default class MediaRoutes {
}
async remove(ctx) {
let site = this.getSite(ctx)
let site = await this.security.verifyToken(ctx)
this.filesCacheRemove(site, ctx.params.filename)
let root = path.join(config.get('uploadFolder'), site)
var unlinkPath = path.join(root, decodeURIComponent(ctx.params.filename))
if (unlinkPath.indexOf(root) !== 0) {
throw new HttpError(403, `Error removing ${unlinkPath}: Traversing folder is not allowed`)
}
await this.fs.unlink(unlinkPath)
await this.fs.unlink(`${config.get('uploadFolder')}/${site}/${ctx.params.filename}`)
.catch(function(err) {
throw new HttpError(422, `Error removing ${unlinkPath}: ${err.message}`)
throw new HttpError(422, `Error removing ${site}/${ctx.params.filename}: ${err.message}`)
})
ctx.status = 204

View file

@ -25,24 +25,6 @@ export function verifyToken(ctx) {
}
}
export function hasCors(site) {
let sites = config.get('sites')
return sites[site]?.cors === true
}
export function verifyCorsEnabled(ctx) {
let site
try {
site = verifyToken(ctx)
} catch (err) {
throw new HttpError(404)
}
if (!hasCors(site)) {
throw new HttpError(404)
}
}
export function throwIfNotPublic(site) {
let sites = config.get('sites')
if (!sites[site] || sites[site].public !== true) {
@ -70,9 +52,8 @@ export function verifyBody(ctx) {
}
let item = ctx.req.body[key]
if (item == null) continue
if (typeof(item) !== 'object'
|| !item
|| Array.isArray(item)) {
throw new HttpError(422, `Body item ${key} was not valid`)
}

View file

@ -43,7 +43,7 @@ export default class Server {
})
let healthChecks = 0
let healthCollectLimit = 60 * 60 * 12
let healthCollectLimit = 10
this.flaska.after(function(ctx) {
let ended = performance.now() - ctx.__started
@ -60,7 +60,7 @@ export default class Server {
if (ctx.url === '/health') {
healthChecks++
if (healthChecks >= healthCollectLimit) {
if (healthChecks > healthCollectLimit) {
ctx.log[level]({
duration: Math.round(ended),
status: ctx.status,

View file

@ -66,7 +66,7 @@ on_success:
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets
echo "Deplying to production"
curl -X POST http://192.168.93.51:4010/update/storageupload
curl -X POST http://192.168.93.50:4010/update/storageupload
fi
# on build failure

View file

@ -1,6 +1,6 @@
{
"name": "storage-upload",
"version": "2.2.12",
"version": "2.2.3",
"description": "Micro service for uploading and image resizing files to a storage server.",
"main": "index.js",
"scripts": {
@ -42,7 +42,7 @@
},
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
"dependencies": {
"flaska": "^1.3.5",
"flaska": "^1.2.3",
"formidable": "^1.2.2",
"nconf-lite": "^2.0.0",
"sharp": "^0.30.3"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View file

@ -26,9 +26,6 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
host: urlObj.hostname,
port: Number(urlObj.port),
path: urlObj.pathname + urlObj.search,
headers: {
origin: 'http://localhost'
}
}))
const req = http.request(opts)
@ -47,9 +44,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
})
res.on('end', function () {
let headers = opts.includeHeaders ? res.headers : null
if (!output) return resolve(headers ? { headers } : null)
if (!output) return resolve(null)
try {
output = JSON.parse(output)
} catch (e) {
@ -61,29 +56,29 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) {
err.status = output.status
return reject(err)
}
resolve(headers ? { headers, output } : output)
resolve(output)
})
})
req.end()
})
}
Client.prototype.get = function(url = '/', options) {
return this.customRequest('GET', url, null, options)
Client.prototype.get = function(url = '/') {
return this.customRequest('GET', url, null)
}
Client.prototype.post = function(url = '/', body = {}, options) {
Client.prototype.post = function(url = '/', body = {}) {
let parsed = JSON.stringify(body)
return this.customRequest('POST', url, parsed, defaults(options, {
return this.customRequest('POST', url, parsed, {
headers: {
'Content-Type': 'application/json',
'Content-Length': parsed.length,
},
}))
})
}
Client.prototype.del = function(url = '/', body = {}, options) {
return this.customRequest('DELETE', url, JSON.stringify(body), options)
Client.prototype.del = function(url = '/', body = {}) {
return this.customRequest('DELETE', url, JSON.stringify(body))
}
const random = (length = 8) => {
@ -97,7 +92,7 @@ const random = (length = 8) => {
return str;
}
Client.prototype.upload = function(url, file, method = 'POST', body = {}, options) {
Client.prototype.upload = function(url, file, method = 'POST', body = {}) {
return fs.readFile(file).then(data => {
const crlf = '\r\n'
const filename = path.basename(file)
@ -119,12 +114,12 @@ Client.prototype.upload = function(url, file, method = 'POST', body = {}, option
Buffer.from(`${crlf}--${boundary}--`),
])
return this.customRequest(method, url, multipartBody, defaults(options, {
return this.customRequest(method, url, multipartBody, {
timeout: 5000,
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Content-Length': multipartBody.length,
},
}))
})
})
}

View file

@ -35,25 +35,12 @@ export function startServer() {
"default@HS256": "asdf1234"
}
},
"development_cors": {
"keys": {
"default@HS256": "asdf1234"
},
"cors": true
},
"existing": {
"public": true,
"keys": {
"default@HS256": "asdf1234"
}
},
"existing_cors": {
"public": true,
"keys": {
"default@HS256": "asdf1234"
},
"cors": true
},
}
},
})

View file

@ -15,7 +15,7 @@ function resolve(file) {
const currYear = new Date().getFullYear().toString()
t.timeout(10000).describe('Media (API)', () => {
t.describe('Media (API)', () => {
let client
let secret = 'asdf1234'
let testFiles = []
@ -34,82 +34,7 @@ t.timeout(10000).describe('Media (API)', () => {
})
})
t.describe('OPTIONS', function() {
t.test('should fail options for every single route', async () => {
const testPaths = [
'/media',
'/media/:site',
'/media',
'/media/noprefix',
'/media/resize',
'/media/resize/:filename',
'/media/:filename',
]
for (let path of testPaths) {
let err = await assert.isRejected(
client.customRequest('OPTIONS', path, null)
)
assert.strictEqual(err.status, 404)
}
})
t.test('should fail options for every single route even with token', async () => {
let token = encode(null, { iss: 'development' }, secret)
const testPaths = [
'/media',
'/media/:site',
'/media',
'/media/noprefix',
'/media/resize',
'/media/resize/:filename',
'/media/:filename',
]
for (let path of testPaths) {
let err = await assert.isRejected(
client.customRequest('OPTIONS', path + `?token=${token}`, null)
)
assert.strictEqual(err.status, 404)
}
})
t.test('should work if specified has cors enabled', async () => {
const assertOrigin = 'http://localhost:9999'
let token = encode(null, { iss: 'development_cors' }, secret)
const testPaths = [
'/media',
'/media/:site',
'/media',
'/media/noprefix',
'/media/resize',
'/media/resize/:filename',
'/media/:filename',
]
for (let path of testPaths) {
let res = await client.customRequest('OPTIONS', path + `?token=${token}`, null, {
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
})
assert.strictEqual(res.headers['access-control-allow-origin'], assertOrigin)
assert.match(res.headers['access-control-allow-methods'], /post/i)
}
})
})
t.describe('POST /media', function temp() {
let config
t.before(async () => {
config = (await import('../../api/config.mjs')).default
})
t.afterEach(function() {
config.sources[2].store.fileSize = 524288000
delete config.sources[1].store.sites.development.fileSize
})
t.timeout(10000).describe('POST /media', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -160,108 +85,6 @@ t.timeout(10000).describe('Media (API)', () => {
t.test('should upload file and create file', async () => {
let token = encode(null, { iss: 'development' }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media?token=${token}`,
resolve('test.png'),
'POST',
{ },
{ includeHeaders: true }
)
)
testFiles.push(data?.output?.path)
assert.ok(data.output)
assert.ok(data.output.filename)
assert.ok(data.output.filename.startsWith(currYear))
assert.ok(data.output.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
fs.stat(resolve(`../../public/${data.output.path}`)),
])
assert.strictEqual(stats[0].size, stats[1].size)
let img = await sharp(resolve(`../../public/${data.output.path}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
assert.notOk(data.headers['access-control-allow-origin'])
})
t.test('should upload file and create file and return cors if site has cors', async () => {
const assertOrigin = 'http://localhost:9000'
let token = encode(null, { iss: 'development_cors' }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media?token=${token}`,
resolve('test.png'),
'POST',
{ },
{
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
}
)
)
testFiles.push(data.output?.path)
assert.ok(data.output)
assert.ok(data.output.filename)
assert.ok(data.output.filename.startsWith(currYear))
assert.ok(data.output.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
fs.stat(resolve(`../../public/${data.output.path}`)),
])
assert.strictEqual(stats[0].size, stats[1].size)
let img = await sharp(resolve(`../../public/${data.output.path}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
assert.strictEqual(data.headers['access-control-allow-origin'], assertOrigin)
})
t.test('should properly replace spaces and other stuff', async () => {
let token = encode(null, { iss: 'development' }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media?token=${token}`,
resolve('A stray\'d cat asked to go & inside a shop during heatwave [oBv38cS-MbM].mp4_snapshot_00.00.164.png')
)
)
testFiles.push(data.path)
assert.ok(data)
assert.ok(data.filename)
assert.ok(data.filename.startsWith(currYear))
assert.ok(data.path)
assert.notOk(data.filename.includes(' '))
assert.ok(data.filename.includes('A_strayd_cat_asked_to_go_and_inside_a_shop_during_heatwave_[oBv38cS-MbM].mp4_snapshot_00.00.164.png'))
let stats = await Promise.all([
fs.stat(resolve('A stray\'d cat asked to go & inside a shop during heatwave [oBv38cS-MbM].mp4_snapshot_00.00.164.png')),
fs.stat(resolve(`../../public/${data.path}`)),
])
assert.strictEqual(stats[0].size, stats[1].size)
let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
assert.strictEqual(img.width, 1280)
assert.strictEqual(img.height, 720)
assert.strictEqual(img.format, 'png')
})
t.test('should return cors for cors-enabled sites', async () => {
let token = encode(null, { iss: 'development' }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media?token=${token}`,
@ -287,38 +110,9 @@ t.timeout(10000).describe('Media (API)', () => {
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
})
t.test('should support site-specific fileSize', async () => {
let token = encode(null, { iss: 'development' }, secret)
config.sources[2].store.fileSize = 1000
let err = await assert.isRejected(
client.upload(
`/media?token=${token}`,
resolve('test.png')
)
)
assert.match(err.message, /maxFileSize exceeded/)
config.sources[1].store.sites.development.fileSize = 524288000
let data = await assert.isFulfilled(
client.upload(
`/media?token=${token}`,
resolve('test.png')
)
)
testFiles.push(data.path)
assert.ok(data)
assert.ok(data.filename)
assert.ok(data.filename.startsWith(currYear))
})
})
t.describe('POST /media/noprefix', function temp() {
t.timeout(10000).describe('POST /media/noprefix', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -372,18 +166,15 @@ t.timeout(10000).describe('Media (API)', () => {
let data = await assert.isFulfilled(
client.upload(
`/media/noprefix?token=${token}`,
resolve('test.png'),
'POST',
{ },
{ includeHeaders: true }
resolve('test.png')
)
)
testFiles.push(data?.output?.path)
testFiles.push(data.path)
assert.ok(data.output)
assert.strictEqual(data.output.filename, 'test.png')
assert.ok(data.output.path)
assert.ok(data)
assert.strictEqual(data.filename, 'test.png')
assert.ok(data.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
@ -395,44 +186,6 @@ t.timeout(10000).describe('Media (API)', () => {
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
assert.notOk(data.headers['access-control-allow-origin'])
})
t.test('should upload and create file with no prefix', async () => {
const assertOrigin = 'http://localhost:9000'
let token = encode(null, { iss: 'development_cors' }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media/noprefix?token=${token}`,
resolve('test.png'),
'POST',
{ },
{
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
}
)
)
testFiles.push(data?.output?.path)
assert.ok(data.output)
assert.strictEqual(data.output.filename, 'test.png')
assert.ok(data.output.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
fs.stat(resolve('../../public/development/test.png')),
])
assert.strictEqual(stats[0].size, stats[1].size)
let img = await sharp(resolve('../../public/development/test.png')).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
assert.strictEqual(data.headers['access-control-allow-origin'], assertOrigin)
})
t.test('should upload and create file with prefix if exists', async () => {
@ -475,7 +228,7 @@ t.timeout(10000).describe('Media (API)', () => {
})
})
t.describe('POST /media/resize', function temp() {
t.timeout(10000).describe('POST /media/resize', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -531,66 +284,27 @@ t.timeout(10000).describe('Media (API)', () => {
`/media/resize?token=${token}`,
resolve('test.png'),
'POST',
{ },
{ includeHeaders: true }
{ }
)
)
testFiles.push(data?.output?.path)
testFiles.push(data.path)
assert.ok(data.output)
assert.ok(data.output.filename)
assert.ok(data.output.filename.startsWith(currYear))
assert.ok(data.output.path)
assert.ok(data)
assert.ok(data.filename)
assert.ok(data.filename.startsWith(currYear))
assert.ok(data.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
fs.stat(resolve(`../../public/${data.output.path}`)),
fs.stat(resolve(`../../public/${data.path}`)),
])
assert.strictEqual(stats[0].size, stats[1].size)
let img = await sharp(resolve(`../../public/${data.output.path}`)).metadata()
let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
assert.notOk(data.headers['access-control-allow-origin'])
})
t.test('should upload file and create file and return cors if site has cors', async () => {
const assertOrigin = 'http://localhost:9000'
let token = encode(null, { iss: 'development_cors' }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media/resize?token=${token}`,
resolve('test.png'),
'POST',
{ },
{
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
}
)
)
testFiles.push(data?.output?.path)
assert.ok(data.output)
assert.ok(data.output.filename)
assert.ok(data.output.filename.startsWith(currYear))
assert.ok(data.output.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
fs.stat(resolve(`../../public/${data.output.path}`)),
])
assert.strictEqual(stats[0].size, stats[1].size)
let img = await sharp(resolve(`../../public/${data.output.path}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
assert.strictEqual(data.headers['access-control-allow-origin'], assertOrigin)
})
t.test('should upload file and create multiple sizes for file', async () => {
@ -717,8 +431,13 @@ t.timeout(10000).describe('Media (API)', () => {
assert.strictEqual(img.height, 12)
assert.strictEqual(img.format, 'jpeg')
})
})
t.test('should upload file and filter out null sizes', async () => {
t.timeout(10000).describe('POST /media/resize/:filename', function temp() {
let sourceFilename
let sourcePath
t.before(async function() {
let token = encode(null, { iss: 'development' }, secret)
let data = await assert.isFulfilled(
@ -726,70 +445,19 @@ t.timeout(10000).describe('Media (API)', () => {
`/media/resize?token=${token}`,
resolve('test.png'),
'POST',
{
outtest: null,
bla: null,
test: null,
}
{ }
)
)
assert.ok(data)
assert.ok(data.filename)
assert.ok(data.path)
testFiles.push(data.path)
let stats = await Promise.all([
fs.stat(resolve('test.png')),
fs.stat(resolve(`../../public/${data.path}`)),
])
assert.strictEqual(stats[0].size, stats[1].size)
assert.ok(data)
assert.ok(data.filename)
assert.ok(data.filename.startsWith(currYear))
assert.ok(data.path)
let img = await sharp(resolve(`../../public/${data.path}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
})
})
t.describe('POST /media/resize/:filename', function temp() {
let sourceFilename
let sourceFilenameCors
let sourcePath
let sourcePathCors
t.before(async function() {
let issuers = ['development', 'development_cors']
for (let iss of issuers) {
let token = encode(null, { iss }, secret)
let data = await assert.isFulfilled(
client.upload(
`/media/resize?token=${token}`,
resolve('test.png'),
'POST',
{ }
)
)
testFiles.push(data.path)
assert.ok(data)
assert.ok(data.filename)
assert.ok(data.filename.startsWith(currYear))
assert.ok(data.path)
if (iss === 'development') {
sourceFilename = data.filename
sourcePath = data.path
} else {
sourceFilenameCors = data.filename
sourcePathCors = data.path
}
}
sourceFilename = data.filename
sourcePath = data.path
})
t.test('should require authentication', async () => {
@ -865,75 +533,34 @@ t.timeout(10000).describe('Media (API)', () => {
compressionLevel: 9,
}
},
},
{ includeHeaders: true }
}
)
)
testFiles.push(data.output.test1.path)
testFiles.push(data.output.test2.path)
testFiles.push(data.test1.path)
testFiles.push(data.test2.path)
assert.ok(data.output.test1.filename)
assert.ok(data.output.test1.filename.startsWith(currYear))
assert.ok(data.output.test1.path)
assert.ok(data.output.test2.filename)
assert.ok(data.output.test2.filename.startsWith(currYear))
assert.ok(data.output.test2.path)
assert.ok(data.test1.filename)
assert.ok(data.test1.filename.startsWith(currYear))
assert.ok(data.test1.path)
assert.ok(data.test2.filename)
assert.ok(data.test2.filename.startsWith(currYear))
assert.ok(data.test2.path)
let img = await sharp(resolve(`../../public/${sourcePath}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
img = await sharp(resolve(`../../public/${data.output.test1.path}`)).metadata()
img = await sharp(resolve(`../../public/${data.test1.path}`)).metadata()
assert.strictEqual(img.width, 320)
assert.strictEqual(img.height, 413)
assert.strictEqual(img.format, 'jpeg')
img = await sharp(resolve(`../../public/${data.output.test2.path}`)).metadata()
img = await sharp(resolve(`../../public/${data.test2.path}`)).metadata()
assert.strictEqual(img.width, 150)
assert.strictEqual(img.height, 175)
assert.strictEqual(img.format, 'png')
assert.notOk(data.headers['access-control-allow-origin'])
})
t.test('should create sizes for existing file and return cors if site has cors', async () => {
const assertOrigin = 'http://localhost:9000'
let token = encode(null, { iss: 'development_cors' }, secret)
let data = await assert.isFulfilled(
client.post(
`/media/resize/${sourceFilenameCors}?token=${token}`,
{
test2: {
format: 'png',
resize: { width: 150 },
png: { compressionLevel: 9 }
},
},
{
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
}
)
)
testFiles.push(data.output.test2.path)
assert.ok(data.output.test2.filename)
assert.ok(data.output.test2.filename.startsWith(currYear))
assert.ok(data.output.test2.path)
let img = await sharp(resolve(`../../public/${sourcePath}`)).metadata()
assert.strictEqual(img.width, 600)
assert.strictEqual(img.height, 700)
assert.strictEqual(img.format, 'png')
img = await sharp(resolve(`../../public/${data.output.test2.path}`)).metadata()
assert.strictEqual(img.width, 150)
assert.strictEqual(img.height, 175)
assert.strictEqual(img.format, 'png')
assert.strictEqual(data.headers['access-control-allow-origin'], assertOrigin)
})
t.test('should base64 output of existing file', async () => {
@ -970,7 +597,7 @@ t.timeout(10000).describe('Media (API)', () => {
})
})
t.describe('DELETE /media/:filename', function temp() {
t.timeout(10000).describe('DELETE /media/:filename', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -1019,7 +646,7 @@ t.timeout(10000).describe('Media (API)', () => {
})
t.test('should remove the file', async () => {
let token = encode(null, { iss: 'development' }, secret)
let token = encode(null, { iss: 'existing' }, secret)
let data = await client.upload(
`/media/noprefix?token=${token}`,
@ -1029,11 +656,11 @@ t.timeout(10000).describe('Media (API)', () => {
let filepath = data.path
testFiles.push(filepath)
let files = await client.get('/media?token=' + token)
let files = await client.get('/media/existing')
let found = false
for (let file of files) {
if (file.filename === data.filename) {
if (file.filename === 'test.png') {
found = true
}
}
@ -1043,70 +670,17 @@ t.timeout(10000).describe('Media (API)', () => {
fs.stat(resolve(`../../public/${filepath}`))
)
let res = await assert.isFulfilled(
client.del(`/media/${data.filename}?token=${token}`, {}, {
includeHeaders: true
})
)
testFiles.splice(testFiles.length - 1)
files = await client.get('/media?token=' + token)
found = false
for (let file of files) {
if (file.filename === data.filename) {
found = true
}
}
assert.notOk(found)
await assert.isRejected(
fs.stat(resolve(`../../public/${filepath}`))
)
assert.notOk(res.headers['access-control-allow-origin'])
})
t.test('should remove the file and return cors if site has cors', async () => {
const assertOrigin = 'http://localhost:9000'
let token = encode(null, { iss: 'development_cors' }, secret)
let data = await client.upload(
`/media/noprefix?token=${token}`,
resolve('test.png')
)
let filepath = data.path
testFiles.push(filepath)
let files = await client.get('/media?token=' + token)
let found = false
for (let file of files) {
if (file.filename === data.filename) {
found = true
}
}
assert.ok(found)
await assert.isFulfilled(
fs.stat(resolve(`../../public/${filepath}`))
)
let res = await assert.isFulfilled(
client.del(`/media/${data.filename}?token=${token}`, {}, {
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
})
client.del(`/media/test.png?token=${token}`)
)
testFiles.splice(testFiles.length - 1)
files = await client.get('/media?token=' + token)
files = await client.get('/media/existing')
found = false
for (let file of files) {
if (file.filename === data.filename) {
if (file.filename === 'test.png') {
found = true
}
}
@ -1115,7 +689,6 @@ t.timeout(10000).describe('Media (API)', () => {
await assert.isRejected(
fs.stat(resolve(`../../public/${filepath}`))
)
assert.strictEqual(res.headers['access-control-allow-origin'], assertOrigin)
})
})
@ -1162,39 +735,17 @@ t.timeout(10000).describe('Media (API)', () => {
t.test('should return list of files in specified folder', async () => {
let token = encode(null, { iss: 'development' }, secret)
let data = await client.get('/media?token=' + token, { includeHeaders: true })
let data = await client.get('/media?token=' + token)
assert.ok(data.output.length)
assert.ok(data.length)
let found = false
for (let file of data.output) {
if (file.filename === '.gitkeep') {
for (let file of data) {
if (file.filename === '.gitkeep' && file.size === 0) {
found = true
break
}
}
assert.ok(found)
assert.notOk(data.headers['access-control-allow-origin'])
})
t.test('should return list of files in specified folder and return cors if site has cors', async () => {
const assertOrigin = 'http://localhost:9000'
let token = encode(null, { iss: 'development_cors' }, secret)
let data = await client.get('/media?token=' + token, {
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
})
assert.ok(data.output.length)
let found = false
for (let file of data.output) {
if (file.filename === '.gitkeep') {
found = true
break
}
}
assert.ok(found)
assert.strictEqual(data.headers['access-control-allow-origin'], assertOrigin)
})
})
@ -1214,25 +765,11 @@ t.timeout(10000).describe('Media (API)', () => {
})
t.test('should otherwise return list of files for a public site', async () => {
let files = await client.get('/media/existing', { includeHeaders: true })
assert.strictEqual(files.output[0].filename, '20220105_101610_test1.jpg')
assert.strictEqual(files.output[0].size, null)
assert.strictEqual(files.output[1].filename, '20220105_101610_test2.png')
assert.strictEqual(files.output[1].size, null)
assert.notOk(files.headers['access-control-allow-origin'])
})
t.test('should otherwise return list of files for a public site and cors if site has cors', async () => {
const assertOrigin = 'http://localhost:9000'
let files = await client.get('/media/existing_cors', {
includeHeaders: true,
headers: { origin: assertOrigin, 'access-control-request-method': 'POST' },
})
assert.strictEqual(files.output[0].filename, '20220105_101610_test1.jpg')
assert.strictEqual(files.output[0].size, null)
assert.strictEqual(files.output[1].filename, '20220105_101610_test2.png')
assert.strictEqual(files.output[1].size, null)
assert.strictEqual(files.headers['access-control-allow-origin'], assertOrigin)
let files = await client.get('/media/existing')
assert.strictEqual(files[0].filename, '20220105_101610_test1.jpg')
assert.strictEqual(files[0].size, 15079)
assert.strictEqual(files[1].filename, '20220105_101610_test2.png')
assert.strictEqual(files[1].size, 31705)
})
})
})

View file

@ -1,5 +1,4 @@
import fs from 'fs/promises'
import path from 'path'
import { Eltro as t, assert, stub } from 'eltro'
import { HttpError } from 'flaska'
import { createContext } from '../helper.server.mjs'
@ -37,11 +36,11 @@ t.describe('#filesCacheGet', function() {
assert.strictEqual(routes.filesCacheGet('nonexisting').length, 0)
assert.strictEqual(routes.filesCacheGet('development')[0].filename, '.gitkeep')
assert.strictEqual(routes.filesCacheGet('development')[0].size, null)
assert.strictEqual(routes.filesCacheGet('development')[0].size, 0)
assert.strictEqual(routes.filesCacheGet('existing')[0].filename, '20220105_101610_test1.jpg')
assert.strictEqual(routes.filesCacheGet('existing')[0].size, null)
assert.strictEqual(routes.filesCacheGet('existing')[0].size, 15079)
assert.strictEqual(routes.filesCacheGet('existing')[1].filename, '20220105_101610_test2.png')
assert.strictEqual(routes.filesCacheGet('existing')[1].size, null)
assert.strictEqual(routes.filesCacheGet('existing')[1].size, 31705)
})
t.test('should always sort result', async function() {
@ -52,9 +51,9 @@ t.describe('#filesCacheGet', function() {
let files = routes.filesCacheGet('existing')
assert.strictEqual(files[0].filename, '20220105_101610_test1.jpg')
assert.strictEqual(files[0].size, null)
assert.strictEqual(files[0].size, 15079)
assert.strictEqual(files[1].filename, '20220105_101610_test2.png')
assert.strictEqual(files[1].size, null)
assert.strictEqual(files[1].size, 31705)
routes.siteCache.get('existing').push({ filename: '0000.png', size: 0 })
@ -62,9 +61,9 @@ t.describe('#filesCacheGet', function() {
assert.strictEqual(files[0].filename, '0000.png')
assert.strictEqual(files[0].size, 0)
assert.strictEqual(files[1].filename, '20220105_101610_test1.jpg')
assert.strictEqual(files[1].size, null)
assert.strictEqual(files[1].size, 15079)
assert.strictEqual(files[2].filename, '20220105_101610_test2.png')
assert.strictEqual(files[2].size, null)
assert.strictEqual(files[2].size, 31705)
})
})
@ -73,7 +72,7 @@ t.describe('#listFiles()', function() {
const stubGetCache = stub()
const routes = new MediaRoutes({
security: { verifyToken: stubVerify, hasCors: stub() },
security: { verifyToken: stubVerify },
})
routes.filesCacheGet = stubGetCache
@ -87,7 +86,7 @@ t.describe('#listFiles()', function() {
let ctx = createContext()
const assertError = new Error('temp')
stubVerify.throws(assertError)
stubVerify.rejects(assertError)
let err = await assert.isRejected(routes.listFiles(ctx))
@ -102,7 +101,7 @@ t.describe('#listFiles()', function() {
let ctx = createContext()
const assertSiteName = 'benshapiro'
const assertResult = { a: 1 }
stubVerify.returns(assertSiteName)
stubVerify.resolves(assertSiteName)
stubGetCache.returns(assertResult)
await routes.listFiles(ctx)
@ -118,7 +117,7 @@ t.describe('#listPublicFiles()', function() {
const stubGetCache = stub()
const routes = new MediaRoutes({
security: { throwIfNotPublic: stubSitePublic, hasCors: stub() },
security: { throwIfNotPublic: stubSitePublic },
})
routes.filesCacheGet = stubGetCache
@ -250,7 +249,6 @@ t.describe('#uploadNoPrefix', function() {
security: {
verifyToken: stubVerify,
verifyBody: stub(),
hasCors: stub(),
},
formidable: { uploadFile: stubUpload, },
fs: { stat: stubStat },
@ -268,7 +266,7 @@ t.describe('#uploadNoPrefix', function() {
let ctx = createContext()
const assertSiteName = 'benshapiro'
const assertError = new Error('hello')
stubVerify.returns(assertSiteName)
stubVerify.resolves(assertSiteName)
stubUpload.rejects(assertError)
let err = await assert.isRejected(routes.uploadNoPrefix(ctx))
@ -301,7 +299,6 @@ t.describe('#resizeExisting', function() {
security: {
verifyToken: stubVerifyToken,
verifyBody: stubVerifyBody,
hasCors: stub(),
},
fs: { stat: stubStat },
sharp: stubSharp,
@ -340,7 +337,7 @@ t.describe('#resizeExisting', function() {
let ctx = createContext({ req: { body: { } } })
const assertError = new Error('temp')
stubVerifyToken.throws(assertError)
stubVerifyToken.rejects(assertError)
let err = await assert.isRejected(routes.resizeExisting(ctx))
@ -369,7 +366,7 @@ t.describe('#resizeExisting', function() {
const assertJpeg = { a: 1 }
const assertFilename = 'asdfsafd'
const assertSite = 'mario'
stubVerifyToken.returns(assertSite)
stubVerifyToken.resolves(assertSite)
let ctx = createContext({ req: { body: {
[assertKey]: {
@ -420,7 +417,7 @@ t.describe('#resizeExisting', function() {
const assertPayload = { a: 1 }
const assertFilename = 'asdfsafd'
const assertSite = 'mario'
stubVerifyToken.returns(assertSite)
stubVerifyToken.resolves(assertSite)
let called = 0
stubStat.returnWith(function() {
@ -469,7 +466,7 @@ t.describe('#resizeExisting', function() {
const assertValidKey2 = 'derp'
const assertErrorKey = 'throwmyerr'
const assertErrorMessage = 'some message here'
stubVerifyToken.returns('asdf')
stubVerifyToken.resolves('asdf')
let called = 0
stubStat.returnWith(function() {
@ -514,7 +511,7 @@ t.describe('#resizeExisting', function() {
const assertFilename = 'asdfsafd.png'
const assertSite = 'mario'
const assertBase64Data = 'asdf1234'
stubVerifyToken.returns(assertSite)
stubVerifyToken.resolves(assertSite)
let ctx = createContext({ req: { body: {
[assertKey]: {
@ -564,7 +561,6 @@ t.describe('#resize', function() {
security: {
verifyToken: stubVerifyToken,
verifyBody: stubVerifyBody,
hasCors: stub(),
},
fs: { stat: stubStat },
formidable: { uploadFile: stubUpload },
@ -621,7 +617,7 @@ t.describe('#resize', function() {
const assertJpeg = { a: 1 }
const assertFilename = 'asdfsafd'
const assertSite = 'mario'
stubVerifyToken.returns(assertSite)
stubVerifyToken.resolves(assertSite)
stubUpload.resolves({ filename: assertFilename + '.png' })
let ctx = createContext({ req: { body: {
@ -672,7 +668,7 @@ t.describe('#resize', function() {
const assertPayload = { a: 1 }
const assertFilename = 'asdfsafd'
const assertSite = 'mario'
stubVerifyToken.returns(assertSite)
stubVerifyToken.resolves(assertSite)
stubUpload.resolves({ filename: assertFilename + '.png' })
let called = 0
@ -723,7 +719,7 @@ t.describe('#resize', function() {
const assertValidKey2 = 'derp'
const assertErrorKey = 'throwmyerr'
const assertErrorMessage = 'some message here'
stubVerifyToken.returns('asdf')
stubVerifyToken.resolves('asdf')
stubUpload.resolves({ filename: 'file.png' })
let called = 0
@ -768,7 +764,7 @@ t.describe('#resize', function() {
const assertFilename = 'asdfsafd.png'
const assertSite = 'mario'
const assertBase64Data = 'asdf1234'
stubVerifyToken.returns(assertSite)
stubVerifyToken.resolves(assertSite)
stubUpload.resolves({ filename: assertFilename })
let ctx = createContext({ req: { body: {
@ -814,7 +810,6 @@ basicUploadTestRoutes.forEach(function(name) {
security: {
verifyToken: stubVerify,
verifyBody: stub(),
hasCors: stub(),
},
formidable: { uploadFile: stubUpload, },
fs: { stat: stubStat },
@ -831,7 +826,7 @@ basicUploadTestRoutes.forEach(function(name) {
let ctx = createContext({ req: { body: { } } })
const assertError = new Error('temp')
stubVerify.throws(assertError)
stubVerify.rejects(assertError)
let err = await assert.isRejected(routes[name](ctx))
@ -846,7 +841,7 @@ basicUploadTestRoutes.forEach(function(name) {
let ctx = createContext({ req: { body: { } } })
const assertSiteName = 'benshapiro'
const assertError = new Error('hello')
stubVerify.returns(assertSiteName)
stubVerify.resolves(assertSiteName)
stubUpload.rejects(assertError)
let err = await assert.isRejected(routes[name](ctx))
@ -864,7 +859,7 @@ basicUploadTestRoutes.forEach(function(name) {
const assertSize = 1241412
const assertFilename = 'asdfsafd'
const assertSite = 'mario'
stubVerify.returns(assertSite)
stubVerify.resolves(assertSite)
stubUpload.resolves({ filename: assertFilename })
stubStat.resolves({ size: assertSize })
await routes[name](ctx)
@ -891,7 +886,6 @@ t.describe('#remove()', function() {
const routes = new MediaRoutes({
security: {
verifyToken: stubVerify,
hasCors: stub(),
},
fs: { unlink: stubUnlink },
})
@ -910,7 +904,7 @@ t.describe('#remove()', function() {
let ctx = createContext({ req: { body: { } } })
const assertError = new Error('temp')
stubVerify.throws(assertError)
stubVerify.rejects(assertError)
let err = await assert.isRejected(routes.remove(ctx))
@ -929,7 +923,7 @@ t.describe('#remove()', function() {
let ctx = createContext({ req: { body: { } } })
ctx.params.filename = assertFilename
stubVerify.returns(assertSiteName)
stubVerify.resolves(assertSiteName)
stubUnlink.rejects(assertError)
let err = await assert.isRejected(routes.remove(ctx))
@ -942,7 +936,7 @@ t.describe('#remove()', function() {
assert.match(err.message, new RegExp(assertFilename))
assert.match(err.message, new RegExp(assertErrorMessage))
assert.strictEqual(stubUnlink.firstCall[0], path.join(`./public/${assertSiteName}/`, assertFilename))
assert.strictEqual(stubUnlink.firstCall[0], `./public/${assertSiteName}/${assertFilename}`)
})
t.test('should otherwise return 204 status and remove from array', async function() {
@ -953,7 +947,7 @@ t.describe('#remove()', function() {
let ctx = createContext({ req: { body: { } } })
ctx.params.filename = assertFilename
stubVerify.returns(assertSiteName)
stubVerify.resolves(assertSiteName)
await routes.remove(ctx)

View file

@ -2,7 +2,7 @@ import { Eltro as t, assert} from 'eltro'
import { HttpError } from 'flaska'
import { createContext } from '../helper.server.mjs'
import { verifyToken, verifyBody, throwIfNotPublic, verifyCorsEnabled } from '../../api/media/security.mjs'
import { verifyToken, verifyBody, throwIfNotPublic } from '../../api/media/security.mjs'
import encode from '../../api/jwt/encode.mjs'
import config from '../../api/config.mjs'
@ -61,98 +61,6 @@ t.describe('#throwIfNotPublic()', function() {
})
})
t.describe('#verifyCorsEnabled()', function() {
let ctx
let backup = {}
t.beforeEach(function() {
ctx = createContext({ })
})
t.before(function() {
backup = config.sources[1].store
config.sources[1].store = {
sites: {
justatest: {
keys: {
'default@HS512': 'mysharedkey',
}
},
justatest2: {
keys: {
'default@HS512': 'mysharedkey',
},
cors: false,
},
justatest3: {
keys: {
'default@HS512': 'mysharedkey',
},
cors: true,
},
},
}
})
t.after(function() {
config.sources[1].store = backup
})
t.test('should throw 404 for sites that do not exist or are null', function() {
let tests = [
'justatest',
'justatest2',
'nonexisting1',
null,
]
tests.forEach(function(test) {
ctx.query.set('token', encode({ typ: 'JWT', alg: 'HS512' }, { iss: test }, 'mysharedkey'))
assert.throws(function() { verifyCorsEnabled(ctx) }, function(err) {
assert.ok(err instanceof HttpError)
assert.ok(err instanceof Error)
assert.strictEqual(err.status, 404)
assert.notOk(err.message)
return true
}, `should throw with site ${test}`)
})
})
t.test('should throw 404 for sites that use wrong token', function() {
let tests = [
'justatest',
'justatest2',
'nonexisting1',
null,
]
tests.forEach(function(test) {
ctx.query.set('token', encode({ typ: 'JWT', alg: 'HS512' }, { iss: test }, 'herp'))
assert.throws(function() { verifyCorsEnabled(ctx) }, function(err) {
assert.ok(err instanceof HttpError)
assert.ok(err instanceof Error)
assert.strictEqual(err.status, 404)
assert.notOk(err.message)
return true
}, `should throw with site ${test}`)
})
})
t.test('should pass for sites that have cors enabled', function() {
let tests = [
'justatest3',
]
tests.forEach(function(test) {
ctx.query.set('token', encode({ typ: 'JWT', alg: 'HS512' }, { iss: test }, 'mysharedkey'))
assert.doesNotThrow(function() { verifyCorsEnabled(ctx) }, `should not throw with site ${test}`)
})
})
})
t.describe('#verifyToken()', function() {
let backup = {}
t.before(function() {
@ -252,12 +160,6 @@ t.describe('#verifyBody()', function() {
verifyBody(ctx)
})
t.test('should succeed with null values in body', function() {
let ctx = createContext({ req: { body: { test: null } } })
verifyBody(ctx)
})
t.test('should fail with invalid body', function() {
let ctx = createContext({ req: { body: {
@ -265,7 +167,7 @@ t.describe('#verifyBody()', function() {
} } })
let tests = [
// [null, 'null'],
[null, 'null'],
['', 'empty string'],
['asdf', 'string'],
[0, 'empty number'],