Add cors support for sites that request it
continuous-integration/appveyor/branch AppVeyor build succeeded Details

This commit is contained in:
Jonatan Nilsson 2023-11-15 08:51:48 +00:00
parent ec81950c42
commit 81c9603ea0
8 changed files with 180 additions and 12 deletions

3
.gitignore vendored
View File

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

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 } from 'flaska'
import { HttpError, CorsHandler } from 'flaska'
import * as security from './security.mjs'
import * as formidable from './formidable.mjs'
@ -30,6 +30,7 @@ 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], CorsHandler({}))
}
init(server) {

View File

@ -25,6 +25,20 @@ export function verifyToken(ctx) {
}
}
export function verifyCorsEnabled(ctx) {
let site
try {
site = verifyToken(ctx)
} catch (err) {
throw new HttpError(404)
}
let sites = config.get('sites')
if (!sites[site] || sites[site].cors !== true) {
throw new HttpError(404)
}
}
export function throwIfNotPublic(site) {
let sites = config.get('sites')
if (!sites[site] || sites[site].public !== true) {

View File

@ -1,6 +1,6 @@
{
"name": "storage-upload",
"version": "2.2.7",
"version": "2.2.8",
"description": "Micro service for uploading and image resizing files to a storage server.",
"main": "index.js",
"scripts": {

View File

View File

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

View File

@ -15,7 +15,7 @@ function resolve(file) {
const currYear = new Date().getFullYear().toString()
t.describe('Media (API)', () => {
t.timeout(10000).describe('Media (API)', () => {
let client
let secret = 'asdf1234'
let testFiles = []
@ -34,7 +34,65 @@ t.describe('Media (API)', () => {
})
})
t.timeout(10000).describe('POST /media', function temp() {
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 () => {
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) {
await client.customRequest('OPTIONS', path + `?token=${token}`, null)
}
})
})
t.describe('POST /media', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -144,7 +202,7 @@ t.describe('Media (API)', () => {
})
})
t.timeout(10000).describe('POST /media/noprefix', function temp() {
t.describe('POST /media/noprefix', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -260,7 +318,7 @@ t.describe('Media (API)', () => {
})
})
t.timeout(10000).describe('POST /media/resize', function temp() {
t.describe('POST /media/resize', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)
@ -500,7 +558,7 @@ t.describe('Media (API)', () => {
})
})
t.timeout(10000).describe('POST /media/resize/:filename', function temp() {
t.describe('POST /media/resize/:filename', function temp() {
let sourceFilename
let sourcePath
@ -664,7 +722,7 @@ t.describe('Media (API)', () => {
})
})
t.timeout(10000).describe('DELETE /media/:filename', function temp() {
t.describe('DELETE /media/:filename', function temp() {
t.test('should require authentication', async () => {
resetLog()
assert.strictEqual(log.error.callCount, 0)

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 } from '../../api/media/security.mjs'
import { verifyToken, verifyBody, throwIfNotPublic, verifyCorsEnabled } from '../../api/media/security.mjs'
import encode from '../../api/jwt/encode.mjs'
import config from '../../api/config.mjs'
@ -61,6 +61,98 @@ 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() {