Skip to content
This repository was archived by the owner on Jul 31, 2025. It is now read-only.

Commit 035dd22

Browse files
author
v1rtl
committed
Add jsonp, cookie, clearCookie, download
1 parent 732a9f8 commit 035dd22

File tree

12 files changed

+330
-26
lines changed

12 files changed

+330
-26
lines changed

app.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { onErrorHandler, ErrorHandler } from './onError.ts'
55
import rg from 'https://esm.sh/regexparam'
66
import { Request, getRouteFromApp } from './request.ts'
77
import { Response } from './response.ts'
8-
import { getURLParams, getPathname } from './parseUrl.ts'
8+
import { getURLParams, getPathname } from './utils/parseUrl.ts'
99
import { extendMiddleware } from './extend.ts'
1010
import { serve, Server } from 'https://deno.land/std/http/server.ts'
1111
import * as path from 'https://deno.land/std/path/mod.ts'
@@ -83,7 +83,7 @@ export class App<
8383
locals: Record<string, string> = {}
8484
noMatchHandler: Handler
8585
onError: ErrorHandler
86-
settings: AppSettings
86+
settings: AppSettings & Record<string, any>
8787
engines: Record<string, TemplateFunc<RenderOptions>> = {}
8888
applyExtensions?: (req: Req, res: Res, next: NextFunction) => void
8989

@@ -102,6 +102,18 @@ export class App<
102102
this.applyExtensions = options?.applyExtensions
103103
}
104104

105+
set(setting: string, value: any) {
106+
this.settings[setting] = value
107+
}
108+
109+
enable(setting: string) {
110+
this.settings[setting] = true
111+
}
112+
113+
disable(setting: string) {
114+
this.settings[setting] = false
115+
}
116+
105117
/**
106118
* Register a template engine with extension
107119
*/
@@ -153,15 +165,11 @@ export class App<
153165
use(...args: UseMethodParams<Req, Res, App>) {
154166
const base = args[0]
155167

156-
const fns: any[] = args.slice(1)
168+
const fns: any[] = args.slice(1).flat()
157169

158170
if (base === '/') {
159171
for (const fn of fns) {
160-
if (Array.isArray(fn)) {
161-
super.use(base, fn)
162-
} else {
163-
super.use(base, fns)
164-
}
172+
super.use(base, fn)
165173
}
166174
} else if (typeof base === 'function' || base instanceof App) {
167175
super.use('/', [base, ...fns])

egg.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"entry": "./app.ts",
55
"description": "0-legacy, tiny & fast web framework as a replacement of Express",
66
"homepage": "https://github.com/talentlessguy/tinyhttp-deno",
7-
"version": "0.0.9",
7+
"version": "0.0.10",
88
"ignore": ["./examples/**/*.ts"],
99
"files": ["./**/*.ts", "README.md"],
1010
"checkFormat": false,

extend.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ import {
3030
setLinksHeader,
3131
setContentType,
3232
formatResponse,
33-
setVaryHeader
33+
setVaryHeader,
34+
attachment,
35+
download,
36+
clearCookie,
37+
setCookie
3438
} from './extensions/res/mod.ts'
35-
39+
import { getQueryParams } from './utils/parseUrl.ts'
3640
import { Response, renderTemplate } from './response.ts'
3741

3842
export const extendMiddleware = <
@@ -50,6 +54,8 @@ export const extendMiddleware = <
5054
res.app = app
5155
}
5256

57+
req.query = getQueryParams(req.url)
58+
5359
req.get = getRequestHeader(req)
5460

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

8389
res.end = end(req, res)
8490
res.send = send<Req, Res>(req, res)
85-
res.sendFile = sendFile<Res>(res)
91+
res.sendFile = sendFile<Req, Res>(req, res)
8692
res.sendStatus = sendStatus(req, res)
8793
res.json = json<Res>(res)
8894
res.setHeader = setHeader<Res>(res)
@@ -93,8 +99,13 @@ export const extendMiddleware = <
9399
res.render = renderTemplate<RenderOptions, Res>(res, app)
94100
res.links = setLinksHeader<Res>(res)
95101
res.type = setContentType<Res>(res)
96-
res.format = formatResponse(req, res, next)
97-
res.vary = setVaryHeader(res)
102+
res.format = formatResponse<Req, Res>(req, res, next)
103+
res.vary = setVaryHeader<Res>(res)
104+
res.download = download<Req, Res>(req, res)
105+
res.attachment = attachment<Res>(res)
106+
107+
res.cookie = setCookie<Req, Res>(req, res)
108+
res.clearCookie = clearCookie<Req, Res>(req, res)
98109

99110
next?.()
100111
}

extensions/res/cookie.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Request as Req } from '../../request.ts'
2+
import { Response as Res } from '../../response.ts'
3+
import { append } from './append.ts'
4+
import * as cookie from 'https://esm.sh/@tinyhttp/cookie'
5+
import { sign } from '../../utils/cookieSignature.ts'
6+
7+
export const setCookie = <Request extends Req = Req, Response extends Res = Res>(
8+
req: Request & {
9+
secret?: string | string[]
10+
},
11+
res: Response
12+
) => (
13+
name: string,
14+
value: string | Record<string, unknown>,
15+
options: cookie.SerializeOptions &
16+
Partial<{
17+
signed: boolean
18+
}> = {}
19+
): Response => {
20+
const secret = req.secret as string
21+
22+
const signed = options.signed || false
23+
24+
if (signed && !secret) throw new Error('cookieParser("secret") required for signed cookies')
25+
26+
let val = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
27+
28+
if (signed) val = 's:' + sign(val, secret)
29+
30+
if (options.maxAge) {
31+
options.expires = new Date(Date.now() + options.maxAge)
32+
options.maxAge /= 1000
33+
}
34+
35+
if (options.path == null) options.path = '/'
36+
37+
append(res)('Set-Cookie', `${cookie.serialize(name, String(val), options)}`)
38+
39+
return res
40+
}
41+
42+
export const clearCookie = <Request extends Req = Req, Response extends Res = Res>(req: Request, res: Response) => (
43+
name: string,
44+
options?: cookie.SerializeOptions
45+
): Response => {
46+
return setCookie<Request, Response>(req, res)(
47+
name,
48+
'',
49+
Object.assign({}, { expires: new Date(1), path: '/' }, options)
50+
)
51+
}

extensions/res/download.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { contentDisposition } from 'https://esm.sh/@tinyhttp/content-disposition'
2+
import { SendFileOptions, sendFile } from './sendFile.ts'
3+
import { resolve, extname } from 'https://deno.land/std/path/mod.ts'
4+
import { setContentType, setHeader } from './headers.ts'
5+
import { Request as Req } from '../../request.ts'
6+
import { Response as Res } from '../../response.ts'
7+
8+
export type DownloadOptions = SendFileOptions &
9+
Partial<{
10+
headers: Record<string, any>
11+
}>
12+
13+
type Callback = (err?: any) => void
14+
15+
export const download = <Request extends Req = Req, Response extends Res = Res>(req: Request, res: Response) => (
16+
path: string,
17+
filename?: string | Callback,
18+
options?: DownloadOptions | Callback,
19+
cb?: Callback
20+
): Response => {
21+
let done = cb
22+
let name: string | null = filename as string
23+
let opts: DownloadOptions | null = options as DownloadOptions
24+
25+
// support function as second or third arg
26+
if (typeof filename === 'function') {
27+
done = filename
28+
name = null
29+
} else if (typeof options === 'function') {
30+
done = options
31+
opts = null
32+
}
33+
34+
// set Content-Disposition when file is sent
35+
const headers: Record<string, any> = {
36+
'Content-Disposition': contentDisposition(name || path)
37+
}
38+
39+
// merge user-provided headers
40+
if (opts && opts.headers) {
41+
for (const key of Object.keys(opts.headers)) {
42+
if (key.toLowerCase() !== 'content-disposition') headers[key] = opts.headers[key]
43+
}
44+
}
45+
46+
// merge user-provided options
47+
opts = { ...opts, headers }
48+
49+
// send file
50+
51+
return sendFile<Request, Response>(req, res)(
52+
opts.root ? path : resolve(path),
53+
opts,
54+
done || (() => undefined)
55+
) as Response
56+
}
57+
58+
export const attachment = <Response extends Res = Res>(res: Response) => (filename?: string): Response => {
59+
if (filename) setContentType(res)(extname(filename))
60+
61+
setHeader(res)('Content-Disposition', contentDisposition(filename))
62+
63+
return res
64+
}

extensions/res/jsonp.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Request } from '../../request.ts'
2+
import { Response } from '../../response.ts'
3+
4+
export type JSONPOptions = Partial<{
5+
escape: boolean
6+
replacer: ((this: any, key: string, value: any) => any) | undefined
7+
spaces: string | number
8+
callbackName: string
9+
}>
10+
11+
function stringify(
12+
value: unknown,
13+
replacer: JSONPOptions['replacer'],
14+
spaces: JSONPOptions['spaces'],
15+
escape: JSONPOptions['escape']
16+
) {
17+
let json = replacer || spaces ? JSON.stringify(value, replacer, spaces) : JSON.stringify(value)
18+
19+
if (escape) {
20+
json = json.replace(/[<>&]/g, (c) => {
21+
switch (c.charCodeAt(0)) {
22+
case 0x3c:
23+
return '\\u003c'
24+
case 0x3e:
25+
return '\\u003e'
26+
case 0x26:
27+
return '\\u0026'
28+
default:
29+
return c
30+
}
31+
})
32+
}
33+
34+
return json
35+
}
36+
37+
/**
38+
* Send JSON response with JSONP callback support
39+
* @param req Request
40+
* @param res Response
41+
* @param app App
42+
*/
43+
export const jsonp = (req: Request, res: Response) => (obj: unknown, opts: JSONPOptions = {}) => {
44+
const val = obj
45+
46+
const { escape, replacer, spaces, callbackName = 'callback' } = opts
47+
48+
let body = stringify(val, replacer, spaces, escape)
49+
50+
let callback = req.query[callbackName]
51+
52+
if (!res.get('Content-Type')) {
53+
res.set('X-Content-Type-Options', 'nosniff')
54+
res.set('Content-Type', 'application/json')
55+
}
56+
57+
// jsonp
58+
if (typeof callback === 'string' && callback.length !== 0) {
59+
res.set('X-Content-Type-Options', 'nosniff')
60+
res.set('Content-Type', 'text/javascript')
61+
62+
// restrict callback charset
63+
callback = callback.replace(/[^[\]\w$.]/g, '')
64+
65+
// replace chars not allowed in JavaScript that are in JSON
66+
body = body.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
67+
68+
// the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
69+
// the typeof check is just to reduce client error noise
70+
body = `/**/ typeof ${callback} === 'function' && ${callback}(${body});`
71+
}
72+
73+
return res.send(body)
74+
}

extensions/res/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export * from './headers.ts'
66
export * from './sendFile.ts'
77
export * from './append.ts'
88
export * from './format.ts'
9+
export * from './download.ts'
10+
export * from './cookie.ts'

0 commit comments

Comments
 (0)