FileResponse: Completely finished all HTTP/1.1 support for file handling including range, preconditions, modified and so much more.
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded

Flaska: Refactored how headers and status are sent.
This commit is contained in:
Jonatan Nilsson 2022-03-26 15:50:18 +00:00
parent cfd56f83ae
commit 499cfa8ce0
10 changed files with 673 additions and 56 deletions

View file

@ -4,6 +4,7 @@ import path from 'path'
import http from 'http'
import stream from 'stream'
import fs from 'fs/promises'
import fsSync from 'fs'
import { URL } from 'url'
import { Buffer } from 'buffer'
import zlib from 'zlib'
@ -294,6 +295,88 @@ export class FileResponse {
this.filepath = filepath
this.stat = stat
}
handleRequest(ctx, useFs = fsSync) {
let etag = '"' + this.stat.ino + '-' + this.stat.size + '-' + this.stat.mtime.getTime() + '"'
let lastModified = this.stat.mtime.toUTCString()
let lastModifiedRounded = Date.parse(lastModified)
if (ctx.req.headers['if-match'] && ctx.req.headers['if-match'] !== etag) {
throw new HttpError(412, `Request if-match pre-condition failed`)
}
if (ctx.req.headers['if-unmodified-since']) {
let check = Date.parse(ctx.req.headers['if-unmodified-since'])
if (!check || check < lastModifiedRounded) {
throw new HttpError(412, `Request if-unmodified-since pre-condition failed`)
}
}
ctx.headers['Etag'] = etag
if (ctx.req.headers['if-none-match']) {
let split = ctx.req.headers['if-none-match'].split(',')
for (let check of split) {
if (check.trim() === etag) {
ctx.status = 304
return null
}
}
} else if (ctx.req.headers['if-modified-since']) {
let check = Date.parse(ctx.req.headers['if-modified-since'])
if (check >= lastModifiedRounded) {
ctx.status = 304
return null
}
}
let readOptions = {}
let size = this.stat.size
if (ctx.req.headers['range']) {
let match = ctx.req.headers['range'].match(/bytes=(\d+)-(\d+)?/)
let ifRange = ctx.req.headers['if-range']
if (ifRange) {
if (ifRange[0] === '"' && ifRange !== etag) {
match = null
} else if (ifRange[0] !== '"') {
let check = Date.parse(ifRange)
if (!check || check < lastModifiedRounded) {
match = null
}
}
}
if (match) {
let start = Number(match[1])
let end = size - 1
if (match[2]) {
end = Math.min(Number(match[2]), size - 1)
}
if (start >= size) {
throw new HttpError(416, `Out of range start ${start} outside of ${size} bounds`)
}
if (start <= end) {
size = end - start + 1
readOptions.start = start
readOptions.end = end
ctx.headers['Content-Range'] = start + '-' + end + '/' + this.stat.size
ctx.status = 206
}
}
}
let ext = path.extname(this.filepath).slice(1)
let found = MimeTypeDb[ext]
if (found) {
ctx.type = found[found.length - 1]
}
let stream = useFs.createReadStream(this.filepath, readOptions)
ctx.headers['Last-Modified'] = lastModified
ctx.headers['Content-Length'] = size
return stream
}
}
export class FlaskaRouter {
@ -579,6 +662,7 @@ ctx.state.nonce = nonce;
}
constructFunction += 'ctx.headers = {'
constructFunction += `'Date': new Date().toUTCString(),`
for (let key of headerKeys) {
if (key === 'Content-Security-Policy' && options.nonce.length) {
let groups = options.defaultHeaders[key].split(';')
@ -822,7 +906,15 @@ ctx.state.nonce = nonce;
}
}
requestEnd(err, ctx) {
requestEnd(orgErr, ctx) {
let err = orgErr
if (ctx.body && ctx.body.handleRequest) {
try {
ctx.body = ctx.body.handleRequest(ctx)
} catch (newErr) {
err = newErr
}
}
if (err) {
try {
this._onerror(err, ctx)
@ -835,7 +927,6 @@ ctx.state.nonce = nonce;
return
}
ctx.res.statusCode = ctx.status
if (statuses.empty[ctx.status]) {
ctx.res.writeHead(ctx.status, ctx.headers)
return ctx.res.end()

View file

@ -1,6 +1,6 @@
{
"name": "flaska",
"version": "1.0.0",
"version": "1.1.0",
"description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.",
"main": "flaska.mjs",
"scripts": {

View file

@ -29,6 +29,12 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options =
headers: {},
}))
if (options.agent) {
opts.agent = options.agent
}
// opts.agent = agent
const req = http.request(opts)
req.on('error', (err) => {
@ -56,13 +62,21 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options =
}
res.on('end', function () {
if (options.getRaw) {
output = {
status: res.statusCode,
data: output,
headers: res.headers,
}
} else {
if (!output) return resolve(null)
try {
output = JSON.parse(output)
} catch (e) {
return reject(new Error(`${e.message} while decoding: ${output}`))
}
if (output.status && typeof(output.status) === 'number') {
}
if (!options.getRaw && output.status && typeof(output.status) === 'number') {
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
err.body = output
return reject(err)

View file

@ -1,5 +1,6 @@
import { Eltro as t, assert, stub } from 'eltro'
import { FileResponse } from '../flaska.mjs'
import { createCtx } from './helper.mjs'
t.test('should add path and stat', function() {
const assertPath = 'some/path/here'
@ -12,8 +13,389 @@ t.test('should add path and stat', function() {
})
t.describe('CreateReader()', function() {
t.test('should return fileReader for file with correct seeking', function() {
const stubCreateReadStream = stub()
const assertPath = '/some/path/here.png'
const assertBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const roundedModified = new Date('2021-04-02T11:22:33')
const assertIno = 3569723027
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
[
'if no range',
function() {},
],
[
'if range start is higher than end',
function(headers) { headers['range'] = 'bytes=2000-1000' },
],
[
'if pre-condition if-match passes',
function(headers) { headers['if-match'] = assertEtag },
],
[
'if pre-condition if-unmodified-since passes',
function(headers) { headers['if-unmodified-since'] = roundedModified.toUTCString(); },
],
[
'if both pre-condition pass',
function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() + 1000).toUTCString(); headers['if-match'] = assertEtag },
],
[
'if range is specified but if-range etag does not match',
function(headers) {
headers['if-range'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"`
headers['range'] = 'bytes=1000-2000'
}
],
[
'if range is specified but if-range modified by is older',
function(headers) {
headers['if-range'] = `${new Date(roundedModified.getTime() - 1000).toUTCString()}`
headers['range'] = 'bytes=1000-2000'
}
],
[
'if range is specified but if-range is garbage',
function(headers) {
headers['if-range'] = `asdf`
headers['range'] = 'bytes=1000-2000'
}
],
]
tests.forEach(function(test){
t.test('return fileReader ' + test[0], function() {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
test[1](ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody)
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.deepStrictEqual(stubCreateReadStream.firstCall[1], {})
assert.strictEqual(ctx.headers['Etag'], assertEtag)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], assertSize)
assert.strictEqual(ctx.type, 'image/png')
assert.notOk(ctx.headers['Content-Range'])
assert.strictEqual(ctx.status, 200)
})
})
t.test('return fileReader with proper length with range', function() {
const assertPath = '/some/path/here.jpg'
const assertBody = { a: 1 }
const assertSize = 2000
const assertmTime = new Date('2021-04-03T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=0-1023'
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody)
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.strictEqual(stubCreateReadStream.firstCall[1].start, 0)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, 1023)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], 1024)
assert.strictEqual(ctx.type, 'image/jpeg')
assert.strictEqual(ctx.headers['Content-Range'], `0-1023/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
t.test('return fileReader with proper length with range and if-range passes', function() {
const assertPath = '/some/path/here.jpg'
const assertBody = { a: 1 }
const assertSize = 2000
const assertmTime = new Date('2021-04-03T11:22:33.777')
const roundedModified = new Date('2021-04-03T11:22:33')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
let tests = [
function(headers) { headers['if-range'] = roundedModified.toUTCString() },
function(headers) { headers['if-range'] = assertEtag },
]
tests.forEach(function(check, i) {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=0-1023'
check(ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody)
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.strictEqual(stubCreateReadStream.firstCall[1].start, 0, `start missing in test ${i + 1}`)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, 1023)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], 1024)
assert.strictEqual(ctx.type, 'image/jpeg')
assert.strictEqual(ctx.headers['Content-Range'], `0-1023/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('return fileReader with proper start if only start is specified', function() {
const assertSize = 2000
const stat = {
size: assertSize,
mtime: new Date('2021-04-03T11:22:33.777'),
ino: 111,
}
const testStart = [0, 1000, 1999]
testStart.forEach(function(start) {
const stubCreateReadStream = stub()
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=' + start + '-'
ctx.body = new FileResponse('file.png', stat)
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(stubCreateReadStream.firstCall[1].start, start)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, assertSize - 1)
assert.strictEqual(ctx.headers['Content-Length'], assertSize - start)
assert.strictEqual(ctx.headers['Content-Range'], `${start}-${assertSize - 1}/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('should default to end if end overflows but start is valid', function() {
const assertSize = 2000
const stat = {
size: assertSize,
mtime: new Date('2021-04-03T11:22:33.777'),
ino: 111,
}
const testEnd = [2000, 3000, 99999]
testEnd.forEach(function(end) {
const stubCreateReadStream = stub()
let ctx = createCtx()
ctx.req.headers['range'] = 'bytes=1000-' + end
ctx.body = new FileResponse('file.png', stat)
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(stubCreateReadStream.firstCall[1].start, 1000)
assert.strictEqual(stubCreateReadStream.firstCall[1].end, 1999)
assert.strictEqual(ctx.headers['Content-Length'], 1000)
assert.strictEqual(ctx.headers['Content-Range'], `1000-${assertSize - 1}/${assertSize}`)
assert.strictEqual(ctx.status, 206)
})
})
t.test('should throw 416 if start is out of range', function() {
const stubCreateReadStream = stub()
let tests = [1000, 2000, 9999]
tests.forEach(function(start) {
let ctx = createCtx()
ctx.body = new FileResponse('file.png', {
size: 1000,
mtime: new Date('2021-04-03T11:22:33.777'),
ino: 111,
})
ctx.req.headers['range'] = `bytes=${start}-`
assert.throws(function() {
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
}, function(err) {
assert.strictEqual(err.status, 416)
assert.match(err.message, new RegExp(1000))
assert.match(err.message, new RegExp(start))
assert.ok(ctx.headers['Etag'])
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
return true
})
})
})
t.test('should return 304 if etag is found', function() {
const assertPath = '/some/path/here.png'
const assertNotBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
let tests = [
assertEtag,
`"asdf", "herp", ${assertEtag}`,
`"asdf", ${assertEtag}, "bla"`,
`${assertEtag}, "hello world"`,
`"asdf","herp",${assertEtag}`,
`"asdf",${assertEtag},"bla"`,
`${assertEtag},"hello world"`,
]
tests.forEach(function(check) {
const stubCreateReadStream = stub().returns(assertNotBody)
let ctx = createCtx()
ctx.req.headers['if-none-match'] = check
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, null)
assert.strictEqual(ctx.status, 304)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
})
})
t.test('should return 304 if-last-modified is found', function() {
const assertPath = '/some/path/here.png'
const assertNotBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
assertmTime.toUTCString(),
'Fri, 02 Apr 2021 11:22:33 GMT',
'Fri, 02 Apr 2021 11:22:34 GMT',
'Fri, 02 Apr 2022 11:22:32 GMT',
]
tests.forEach(function(check) {
const stubCreateReadStream = stub().returns(assertNotBody)
let ctx = createCtx()
ctx.body = new FileResponse(assertPath, stat)
ctx.req.headers['if-modified-since'] = assertmTime.toUTCString()
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, null)
assert.strictEqual(ctx.status, 304)
assert.strictEqual(ctx.headers['Etag'], `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`)
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
})
})
t.test('should return 200 if etag or modified-by is not found', function() {
const assertPath = '/some/path/here.png'
const assertBody = { a: 1 }
const assertSize = 12498
const assertmTime = new Date('2021-04-02T11:22:33.777')
const assertIno = 3569723027
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const roundedModified = new Date('2021-04-02T11:22:33')
let tests = [
function(headers) { headers['if-none-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"`; },
function(headers) { headers['if-none-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() - 1}"`; },
function(headers) { headers['if-none-match'] = `"asdf"`; },
function(headers) { headers['if-none-match'] = `"asdf"`; headers['if-modified-since'] = roundedModified.toUTCString(); },
function(headers) { headers['if-modified-since'] = new Date(roundedModified.getTime() - 1).toUTCString(); },
function(headers) { headers['if-modified-since'] = 'asdfs' },
]
tests.forEach(function(check, i) {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
check(ctx.req.headers)
ctx.body = new FileResponse(assertPath, stat)
let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
assert.strictEqual(checkBody, assertBody, 'missing body in test ' + (i + 1))
assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath)
assert.deepStrictEqual(stubCreateReadStream.firstCall[1], {})
assert.strictEqual(ctx.headers['Etag'], assertEtag)
assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString())
assert.strictEqual(ctx.headers['Content-Length'], assertSize)
assert.strictEqual(ctx.type, 'image/png')
assert.notOk(ctx.headers['Content-Range'])
assert.strictEqual(ctx.status, 200)
})
})
t.test('should return 412 if precondition fails', function() {
const stubCreateReadStream = stub()
const assertmTime = new Date('2021-04-02T11:22:33.777')
const roundedModified = new Date('2021-04-02T11:22:33')
const assertIno = 3569723027
const assertSize = 12498
const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"`
const stat = {
size: assertSize,
mtime: assertmTime,
ino: assertIno,
}
let tests = [
function(headers) { headers['if-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"` },
function(headers) { headers['if-match'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() - 1}"` },
function(headers) { headers['if-match'] = `asdf` },
function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() - 1).toUTCString(); },
function(headers) { headers['if-unmodified-since'] = 'asdf'; },
function(headers) { headers['if-match'] = assertEtag; headers['if-unmodified-since'] = 'asdf'; },
function(headers) { headers['if-match'] = assertEtag; headers['if-unmodified-since'] = new Date(roundedModified.getTime() - 1000).toUTCString(); },
function(headers) { headers['if-match'] = '"bla"'; headers['if-unmodified-since'] = roundedModified.toUTCString(); },
]
tests.forEach(function(check) {
let ctx = createCtx()
ctx.body = new FileResponse('file.png', stat)
check(ctx.req.headers)
assert.throws(function() {
ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream })
}, function(err) {
assert.strictEqual(err.status, 412)
assert.notOk(stubCreateReadStream.called)
assert.notOk(ctx.headers['Last-Modified'])
assert.notOk(ctx.headers['Content-Length'])
assert.notOk(ctx.type)
assert.notOk(ctx.headers['Content-Range'])
return true
})
})
})
})

View file

@ -43,21 +43,25 @@ t.describe('#constructor', function() {
flaska._before[0](ctx)
assert.deepStrictEqual(ctx.headers, {
'Server': 'Flaska',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
})
assert.deepEqual(
Object.keys(ctx.headers).sort(),
['Server','X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
)
assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
assert.strictEqual(flaska._after.length, 0)
})
t.test('should have before ready setting headers on context if defaultHeaders is specified', function() {
const defaultHeaders = {
'Server': 'nginx/1.16.1',
'Date': 'Mon, 21 Mar\' 2022 07:26:01 GMT',
'Content-Type': 'applicat"ion/json; charset=utf-8',
'Content-Length': '1646',
'Connection': 'keep-alive',
@ -75,8 +79,15 @@ t.describe('#constructor', function() {
flaska._before[0](ctx)
assert.notStrictEqual(ctx.headers, defaultHeaders)
assert.deepStrictEqual(ctx.headers, defaultHeaders)
let keys = Object.keys(defaultHeaders)
console.log(Object.keys(ctx.headers).sort())
console.log(keys.sort())
assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1)
for (let key of keys) {
assert.strictEqual(ctx.headers[key], defaultHeaders[key])
}
assert.ok(ctx.headers['Date'])
assert.strictEqual(flaska._after.length, 0)
})
})
@ -111,14 +122,14 @@ t.describe('#_nonce', function() {
assert.strictEqual(flaska._noncesIndex, oldIndex - 1)
assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce)
assert.deepStrictEqual(ctx.headers, {
'Server': 'Flaska',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
})
assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
})
t.test('should always return nonce values even if it runs out in cache', function() {

View file

@ -153,14 +153,18 @@ t.describe('#requestStart()', function() {
assert.ok(ctx.query.get)
assert.ok(ctx.query.set)
assert.ok(ctx.query.delete)
assert.deepStrictEqual(ctx.headers, {
'Server': 'Flaska',
'X-Content-Type-Options': 'nosniff',
'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
})
assert.deepEqual(
Object.keys(ctx.headers).sort(),
['Server','X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort()
)
assert.strictEqual(ctx.headers['Server'], 'Flaska')
assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff')
assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`)
assert.strictEqual(ctx.headers['Cross-Origin-Opener-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Resource-Policy'], 'same-origin')
assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp')
assert.ok(new Date(ctx.headers['Date']).getDate())
cb()
} catch (err) { cb(err) }
}
@ -194,8 +198,12 @@ t.describe('#requestStart()', function() {
try {
assert.ok(err)
assert.strictEqual(err, assertError)
assert.notStrictEqual(ctx.headers, defaultHeaders)
assert.deepStrictEqual(ctx.headers, defaultHeaders)
let keys = Object.keys(defaultHeaders)
assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1)
for (let key of keys) {
assert.strictEqual(ctx.headers[key], defaultHeaders[key])
}
assert.ok(ctx.headers['Date'])
cb()
} catch (err) { cb(err) }
}

View file

@ -1,5 +1,5 @@
import { Eltro as t, spy, assert, stub} from 'eltro'
import { Flaska, FlaskaRouter } from '../flaska.mjs'
import { FileResponse, Flaska, FlaskaRouter } from '../flaska.mjs'
import { fakeHttp, createCtx } from './helper.mjs'
const fakerHttp = fakeHttp()
@ -16,7 +16,6 @@ t.describe('#requestEnd()', function() {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
assert.ok(body)
@ -71,7 +70,6 @@ t.describe('#requestEnd()', function() {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
assert.ok(body)
@ -100,7 +98,6 @@ t.describe('#requestEnd()', function() {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
assert.ok(body)
@ -129,7 +126,6 @@ t.describe('#requestEnd()', function() {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
assert.ok(body)
@ -149,6 +145,52 @@ t.describe('#requestEnd()', function() {
flaska.requestEnd(null, ctx)
})
t.test('call handleRequest if body contains it', function() {
let body = new FileResponse()
let assertBody = { a : 1 }
const ctx = createCtx({
status: 204,
})
ctx.body = body
const assertHandle = stub().returnWith(function(checkCtx, secondary) {
assert.notOk(secondary)
assert.strictEqual(checkCtx, ctx)
assert.notOk(ctx.res.writeHead.called)
return assertBody
})
body.handleRequest = assertHandle
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
assert.ok(assertHandle.called)
assert.ok(ctx.res.writeHead)
assert.strictEqual(ctx.body, assertBody)
})
t.test('call _onerror with error if handleRequest throws error', function() {
let body = new FileResponse()
const assertError = new Error('Secrets of the Goddess')
let assertBody = { a : 1 }
const ctx = createCtx({
status: 204,
})
ctx.body = body
const assertHandle = stub().throws(assertError)
body.handleRequest = assertHandle
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska._onerror = stub().returnWith(function(err, checkCtx) {
assert.strictEqual(err, assertError)
checkCtx.body = assertBody
})
flaska.requestEnd(null, ctx)
assert.ok(assertHandle.called)
assert.ok(flaska._onerror.called)
assert.strictEqual(flaska._onerror.firstCall[0], assertError)
assert.ok(ctx.res.writeHead)
assert.strictEqual(ctx.body, assertBody)
})
t.test('call res and end correctly when dealing with custom type', function(cb) {
const assertStatus = 209
const assertBody = 'test'
@ -156,7 +198,6 @@ t.describe('#requestEnd()', function() {
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], assertType)
assert.strictEqual(body, assertBody)
assert.ok(ctx.res.writeHead.called)
@ -183,7 +224,6 @@ t.describe('#requestEnd()', function() {
let onFinish = function(source, target, callback) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.headers['Content-Type'], assertType)
assert.strictEqual(source, assertBody)
assert.strictEqual(target, ctx.res)
@ -208,7 +248,7 @@ t.describe('#requestEnd()', function() {
t.test('call pipe should have default type', function(cb) {
let onFinish = function(source, target) {
try {
assert.strictEqual(ctx.res.statusCode, 200)
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.headers['Content-Type'], 'application/octet-stream')
assert.strictEqual(source, ctx.body)
assert.strictEqual(target, ctx.res)
@ -237,7 +277,7 @@ t.describe('#requestEnd()', function() {
t.test(`call pipe with file extension ${test[0]} should mimetype ${test[1]}`, function(cb) {
let onFinish = function(source, target) {
try {
assert.strictEqual(ctx.res.statusCode, 200)
assert.strictEqual(ctx.status, 200)
assert.strictEqual(ctx.headers['Content-Type'], test[1])
assert.strictEqual(source, ctx.body)
assert.strictEqual(target, ctx.res)
@ -277,7 +317,6 @@ t.describe('#requestEnd()', function() {
let onFinish = function(body) {
try {
assert.strictEqual(ctx.status, assertStatus)
assert.strictEqual(ctx.res.statusCode, assertStatus)
assert.strictEqual(ctx.res.setHeader.callCount, 0)
assert.ok(ctx.res.writeHead.called)
assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status)

View file

@ -41,7 +41,7 @@ export function createRes(def) {
})
}
export function createCtx(def, endHandler) {
export function createCtx(def = {}, endHandler = null) {
return defaults(def, {
req: createReq(),
res: createRes({ end: endHandler || spy() }),

View file

@ -1,9 +1,10 @@
import fsSync from 'fs'
import http from 'http'
import fs from 'fs/promises'
import formidable from 'formidable'
import { Eltro as t, assert, stub } from 'eltro'
import { setTimeout } from 'timers/promises'
import { Flaska, JsonHandler, FormidableHandler } from '../flaska.mjs'
import { Flaska, JsonHandler, FormidableHandler, FileResponse } from '../flaska.mjs'
import Client from './client.mjs'
const port = 51024
@ -37,6 +38,11 @@ flaska.get('/file', function(ctx) {
file = fsSync.createReadStream('./test/test.png')
ctx.body = file
})
flaska.get('/filehandle', function(ctx) {
return fs.stat('./test/test.txt').then(function(stat) {
ctx.body = new FileResponse('./test/test.txt', stat)
})
})
flaska.post('/file/upload', FormidableHandler(formidable, {
uploadDir: './test/upload'
}), function(ctx) {
@ -139,7 +145,9 @@ t.describe('/timeout', function() {
let err = await assert.isRejected(client.customRequest('GET', '/timeout', JSON.stringify({}), { timeout: 20 }))
await setTimeout(20)
while (!log.info.called) {
await setTimeout(10)
}
assert.match(err.message, /timed out/)
assert.notOk(log.error.called)
@ -156,7 +164,9 @@ t.describe('/file', function() {
await client.customRequest('GET', '/file', null, { toPipe: target })
await setTimeout(20)
while (!target.closed && !file.closed) {
await setTimeout(10)
}
assert.ok(target.closed)
assert.ok(file.closed)
@ -186,8 +196,6 @@ t.describe('/file', function() {
req.destroy()
await setTimeout(20)
while (!file.closed) {
await setTimeout(10)
}
@ -200,6 +208,69 @@ t.describe('/file', function() {
})
})
t.describe('/filehandle', function() {
const agent = new http.Agent({
keepAlive: true,
maxSockets: 1,
keepAliveMsecs: 3000,
})
t.test('server should send correctly', async function() {
log.error.reset()
let res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent })
assert.strictEqual(res.status, 200)
assert.strictEqual(res.headers['content-length'], '11')
assert.strictEqual(res.headers['content-type'], 'text/plain')
assert.match(res.headers['etag'], /\"\d+-11-\d+"/)
assert.ok(res.headers['last-modified'])
let d = new Date(Date.parse(res.headers['last-modified']))
let etag = res.headers['etag']
assert.ok(d.getTime())
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'If-Modified-Since': d.toUTCString()
},
})
assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'If-None-Match': etag
},
})
assert.strictEqual(res.status, 304)
assert.strictEqual(res.data, '')
assert.strictEqual(res.headers['etag'], etag)
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'Range': 'bytes=2-5'
},
})
assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, 'llo ')
assert.strictEqual(res.headers['content-length'], '4')
res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent,
headers: {
'Range': 'bytes=0-0'
},
})
assert.strictEqual(res.status, 206)
assert.strictEqual(res.data, 'H')
assert.strictEqual(res.headers['content-length'], '1')
})
t.after(function() {
agent.destroy()
})
})
t.describe('/file/upload', function() {
t.test('server should upload file', async function() {
let res = await client.upload('/file/upload', './test/test.png')

1
test/test.txt Normal file
View file

@ -0,0 +1 @@
Hello World