Skip to content

Commit

Permalink
Add jsonp, cookie, clearCookie, download
Browse files Browse the repository at this point in the history
  • Loading branch information
v1rtl committed Feb 28, 2021
1 parent 732a9f8 commit 035dd22
Show file tree
Hide file tree
Showing 12 changed files with 330 additions and 26 deletions.
24 changes: 16 additions & 8 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { onErrorHandler, ErrorHandler } from './onError.ts'
import rg from 'https://esm.sh/regexparam'
import { Request, getRouteFromApp } from './request.ts'
import { Response } from './response.ts'
import { getURLParams, getPathname } from './parseUrl.ts'
import { getURLParams, getPathname } from './utils/parseUrl.ts'
import { extendMiddleware } from './extend.ts'
import { serve, Server } from 'https://deno.land/std/http/server.ts'
import * as path from 'https://deno.land/std/path/mod.ts'
Expand Down Expand Up @@ -83,7 +83,7 @@ export class App<
locals: Record<string, string> = {}
noMatchHandler: Handler
onError: ErrorHandler
settings: AppSettings
settings: AppSettings & Record<string, any>
engines: Record<string, TemplateFunc<RenderOptions>> = {}
applyExtensions?: (req: Req, res: Res, next: NextFunction) => void

Expand All @@ -102,6 +102,18 @@ export class App<
this.applyExtensions = options?.applyExtensions
}

set(setting: string, value: any) {
this.settings[setting] = value
}

enable(setting: string) {
this.settings[setting] = true
}

disable(setting: string) {
this.settings[setting] = false
}

/**
* Register a template engine with extension
*/
Expand Down Expand Up @@ -153,15 +165,11 @@ export class App<
use(...args: UseMethodParams<Req, Res, App>) {
const base = args[0]

const fns: any[] = args.slice(1)
const fns: any[] = args.slice(1).flat()

if (base === '/') {
for (const fn of fns) {
if (Array.isArray(fn)) {
super.use(base, fn)
} else {
super.use(base, fns)
}
super.use(base, fn)
}
} else if (typeof base === 'function' || base instanceof App) {
super.use('/', [base, ...fns])
Expand Down
2 changes: 1 addition & 1 deletion egg.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"entry": "./app.ts",
"description": "0-legacy, tiny & fast web framework as a replacement of Express",
"homepage": "https://github.com/talentlessguy/tinyhttp-deno",
"version": "0.0.9",
"version": "0.0.10",
"ignore": ["./examples/**/*.ts"],
"files": ["./**/*.ts", "README.md"],
"checkFormat": false,
Expand Down
21 changes: 16 additions & 5 deletions extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ import {
setLinksHeader,
setContentType,
formatResponse,
setVaryHeader
setVaryHeader,
attachment,
download,
clearCookie,
setCookie
} from './extensions/res/mod.ts'

import { getQueryParams } from './utils/parseUrl.ts'
import { Response, renderTemplate } from './response.ts'

export const extendMiddleware = <
Expand All @@ -50,6 +54,8 @@ export const extendMiddleware = <
res.app = app
}

req.query = getQueryParams(req.url)

req.get = getRequestHeader(req)

if (settings?.freshnessTesting) {
Expand Down Expand Up @@ -82,7 +88,7 @@ export const extendMiddleware = <

res.end = end(req, res)
res.send = send<Req, Res>(req, res)
res.sendFile = sendFile<Res>(res)
res.sendFile = sendFile<Req, Res>(req, res)
res.sendStatus = sendStatus(req, res)
res.json = json<Res>(res)
res.setHeader = setHeader<Res>(res)
Expand All @@ -93,8 +99,13 @@ export const extendMiddleware = <
res.render = renderTemplate<RenderOptions, Res>(res, app)
res.links = setLinksHeader<Res>(res)
res.type = setContentType<Res>(res)
res.format = formatResponse(req, res, next)
res.vary = setVaryHeader(res)
res.format = formatResponse<Req, Res>(req, res, next)
res.vary = setVaryHeader<Res>(res)
res.download = download<Req, Res>(req, res)
res.attachment = attachment<Res>(res)

res.cookie = setCookie<Req, Res>(req, res)
res.clearCookie = clearCookie<Req, Res>(req, res)

next?.()
}
51 changes: 51 additions & 0 deletions extensions/res/cookie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Request as Req } from '../../request.ts'
import { Response as Res } from '../../response.ts'
import { append } from './append.ts'
import * as cookie from 'https://esm.sh/@tinyhttp/cookie'
import { sign } from '../../utils/cookieSignature.ts'

export const setCookie = <Request extends Req = Req, Response extends Res = Res>(
req: Request & {
secret?: string | string[]
},
res: Response
) => (
name: string,
value: string | Record<string, unknown>,
options: cookie.SerializeOptions &
Partial<{
signed: boolean
}> = {}
): Response => {
const secret = req.secret as string

const signed = options.signed || false

if (signed && !secret) throw new Error('cookieParser("secret") required for signed cookies')

let val = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)

if (signed) val = 's:' + sign(val, secret)

if (options.maxAge) {
options.expires = new Date(Date.now() + options.maxAge)
options.maxAge /= 1000
}

if (options.path == null) options.path = '/'

append(res)('Set-Cookie', `${cookie.serialize(name, String(val), options)}`)

return res
}

export const clearCookie = <Request extends Req = Req, Response extends Res = Res>(req: Request, res: Response) => (
name: string,
options?: cookie.SerializeOptions
): Response => {
return setCookie<Request, Response>(req, res)(
name,
'',
Object.assign({}, { expires: new Date(1), path: '/' }, options)
)
}
64 changes: 64 additions & 0 deletions extensions/res/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { contentDisposition } from 'https://esm.sh/@tinyhttp/content-disposition'
import { SendFileOptions, sendFile } from './sendFile.ts'
import { resolve, extname } from 'https://deno.land/std/path/mod.ts'
import { setContentType, setHeader } from './headers.ts'
import { Request as Req } from '../../request.ts'
import { Response as Res } from '../../response.ts'

export type DownloadOptions = SendFileOptions &
Partial<{
headers: Record<string, any>
}>

type Callback = (err?: any) => void

export const download = <Request extends Req = Req, Response extends Res = Res>(req: Request, res: Response) => (
path: string,
filename?: string | Callback,
options?: DownloadOptions | Callback,
cb?: Callback
): Response => {
let done = cb
let name: string | null = filename as string
let opts: DownloadOptions | null = options as DownloadOptions

// support function as second or third arg
if (typeof filename === 'function') {
done = filename
name = null
} else if (typeof options === 'function') {
done = options
opts = null
}

// set Content-Disposition when file is sent
const headers: Record<string, any> = {
'Content-Disposition': contentDisposition(name || path)
}

// merge user-provided headers
if (opts && opts.headers) {
for (const key of Object.keys(opts.headers)) {
if (key.toLowerCase() !== 'content-disposition') headers[key] = opts.headers[key]
}
}

// merge user-provided options
opts = { ...opts, headers }

// send file

return sendFile<Request, Response>(req, res)(
opts.root ? path : resolve(path),
opts,
done || (() => undefined)
) as Response
}

export const attachment = <Response extends Res = Res>(res: Response) => (filename?: string): Response => {
if (filename) setContentType(res)(extname(filename))

setHeader(res)('Content-Disposition', contentDisposition(filename))

return res
}
74 changes: 74 additions & 0 deletions extensions/res/jsonp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Request } from '../../request.ts'
import { Response } from '../../response.ts'

export type JSONPOptions = Partial<{
escape: boolean
replacer: ((this: any, key: string, value: any) => any) | undefined
spaces: string | number
callbackName: string
}>

function stringify(
value: unknown,
replacer: JSONPOptions['replacer'],
spaces: JSONPOptions['spaces'],
escape: JSONPOptions['escape']
) {
let json = replacer || spaces ? JSON.stringify(value, replacer, spaces) : JSON.stringify(value)

if (escape) {
json = json.replace(/[<>&]/g, (c) => {
switch (c.charCodeAt(0)) {
case 0x3c:
return '\\u003c'
case 0x3e:
return '\\u003e'
case 0x26:
return '\\u0026'
default:
return c
}
})
}

return json
}

/**
* Send JSON response with JSONP callback support
* @param req Request
* @param res Response
* @param app App
*/
export const jsonp = (req: Request, res: Response) => (obj: unknown, opts: JSONPOptions = {}) => {
const val = obj

const { escape, replacer, spaces, callbackName = 'callback' } = opts

let body = stringify(val, replacer, spaces, escape)

let callback = req.query[callbackName]

if (!res.get('Content-Type')) {
res.set('X-Content-Type-Options', 'nosniff')
res.set('Content-Type', 'application/json')
}

// jsonp
if (typeof callback === 'string' && callback.length !== 0) {
res.set('X-Content-Type-Options', 'nosniff')
res.set('Content-Type', 'text/javascript')

// restrict callback charset
callback = callback.replace(/[^[\]\w$.]/g, '')

// replace chars not allowed in JavaScript that are in JSON
body = body.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')

// the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
// the typeof check is just to reduce client error noise
body = `/**/ typeof ${callback} === 'function' && ${callback}(${body});`
}

return res.send(body)
}
2 changes: 2 additions & 0 deletions extensions/res/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from './headers.ts'
export * from './sendFile.ts'
export * from './append.ts'
export * from './format.ts'
export * from './download.ts'
export * from './cookie.ts'
Loading

0 comments on commit 035dd22

Please sign in to comment.