deno.land / x / hono@v4.2.5 / utils / cookie.ts

نووسراو ببینە
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import { decodeURIComponent_ } from './url.ts'
export type Cookie = Record<string, string>export type SignedCookie = Record<string, string | false>
type PartitionCookieConstraint = | { partition: true; secure: true } | { partition?: boolean; secure?: boolean } // reset to defaulttype SecureCookieConstraint = { secure: true }type HostCookieConstraint = { secure: true; path: '/'; domain?: undefined }
export type CookieOptions = { domain?: string expires?: Date httpOnly?: boolean maxAge?: number path?: string secure?: boolean signingSecret?: string sameSite?: 'Strict' | 'Lax' | 'None' partitioned?: boolean prefix?: CookiePrefixOptions} & PartitionCookieConstraintexport type CookiePrefixOptions = 'host' | 'secure'
export type CookieConstraint<Name> = Name extends `__Secure-${string}` ? CookieOptions & SecureCookieConstraint : Name extends `__Host-${string}` ? CookieOptions & HostCookieConstraint : CookieOptions
const algorithm = { name: 'HMAC', hash: 'SHA-256' }
const getCryptoKey = async (secret: string | BufferSource): Promise<CryptoKey> => { const secretBuf = typeof secret === 'string' ? new TextEncoder().encode(secret) : secret return await crypto.subtle.importKey('raw', secretBuf, algorithm, false, ['sign', 'verify'])}
const makeSignature = async (value: string, secret: string | BufferSource): Promise<string> => { const key = await getCryptoKey(secret) const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value)) // the returned base64 encoded signature will always be 44 characters long and end with one or two equal signs return btoa(String.fromCharCode(...new Uint8Array(signature)))}
const verifySignature = async ( base64Signature: string, value: string, secret: CryptoKey): Promise<boolean> => { try { const signatureBinStr = atob(base64Signature) const signature = new Uint8Array(signatureBinStr.length) for (let i = 0, len = signatureBinStr.length; i < len; i++) { signature[i] = signatureBinStr.charCodeAt(i) } return await crypto.subtle.verify(algorithm, secret, signature, new TextEncoder().encode(value)) } catch (e) { return false }}
// all alphanumeric chars and all of _!#$%&'*.^`|~+-// (see: https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1)const validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/
// all ASCII chars 32-126 except 34, 59, and 92 (i.e. space to tilde but not double quote, semicolon, or backslash)// (see: https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1)//// note: the spec also prohibits comma and space, but we allow both since they are very common in the real world// (see: https://github.com/golang/go/issues/7243)const validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/
export const parse = (cookie: string, name?: string): Cookie => { const pairs = cookie.trim().split(';') return pairs.reduce((parsedCookie, pairStr) => { pairStr = pairStr.trim() const valueStartPos = pairStr.indexOf('=') if (valueStartPos === -1) { return parsedCookie }
const cookieName = pairStr.substring(0, valueStartPos).trim() if ((name && name !== cookieName) || !validCookieNameRegEx.test(cookieName)) { return parsedCookie }
let cookieValue = pairStr.substring(valueStartPos + 1).trim() if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) { cookieValue = cookieValue.slice(1, -1) } if (validCookieValueRegEx.test(cookieValue)) { parsedCookie[cookieName] = decodeURIComponent_(cookieValue) }
return parsedCookie }, {} as Cookie)}
export const parseSigned = async ( cookie: string, secret: string | BufferSource, name?: string): Promise<SignedCookie> => { const parsedCookie: SignedCookie = {} const secretKey = await getCryptoKey(secret)
for (const [key, value] of Object.entries(parse(cookie, name))) { const signatureStartPos = value.lastIndexOf('.') if (signatureStartPos < 1) { continue }
const signedValue = value.substring(0, signatureStartPos) const signature = value.substring(signatureStartPos + 1) if (signature.length !== 44 || !signature.endsWith('=')) { continue }
const isVerified = await verifySignature(signature, signedValue, secretKey) parsedCookie[key] = isVerified ? signedValue : false }
return parsedCookie}
const _serialize = (name: string, value: string, opt: CookieOptions = {}): string => { let cookie = `${name}=${value}`
if (name.startsWith('__Secure-') && !opt.secure) { // FIXME: replace link to RFC // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-13#section-4.1.3.1 throw new Error('__Secure- Cookie must have Secure attributes') }
if (name.startsWith('__Host-')) { // FIXME: replace link to RFC // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-13#section-4.1.3.2 if (!opt.secure) { throw new Error('__Host- Cookie must have Secure attributes') }
if (opt.path !== '/') { throw new Error('__Host- Cookie must have Path attributes with "/"') }
if (opt.domain) { throw new Error('__Host- Cookie must not have Domain attributes') } }
if (opt && typeof opt.maxAge === 'number' && opt.maxAge >= 0) { if (opt.maxAge > 34560000) { // FIXME: replace link to RFC // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-13#section-4.1.2.2 throw new Error( 'Cookies Max-Age SHOULD NOT be greater than 400 days (34560000 seconds) in duration.' ) } cookie += `; Max-Age=${Math.floor(opt.maxAge)}` }
if (opt.domain && opt.prefix !== 'host') { cookie += `; Domain=${opt.domain}` }
if (opt.path) { cookie += `; Path=${opt.path}` }
if (opt.expires) { if (opt.expires.getTime() - Date.now() > 34560000_000) { // FIXME: replace link to RFC // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-13#section-4.1.2.1 throw new Error( 'Cookies Expires SHOULD NOT be greater than 400 days (34560000 seconds) in the future.' ) } cookie += `; Expires=${opt.expires.toUTCString()}` }
if (opt.httpOnly) { cookie += '; HttpOnly' }
if (opt.secure) { cookie += '; Secure' }
if (opt.sameSite) { cookie += `; SameSite=${opt.sameSite}` }
if (opt.partitioned) { // FIXME: replace link to RFC // https://www.ietf.org/archive/id/draft-cutler-httpbis-partitioned-cookies-01.html#section-2.3 if (!opt.secure) { throw new Error('Partitioned Cookie must have Secure attributes') } cookie += '; Partitioned' }
return cookie}
export const serialize = <Name extends string>( name: Name, value: string, opt?: CookieConstraint<Name>): string => { value = encodeURIComponent(value) return _serialize(name, value, opt)}
export const serializeSigned = async ( name: string, value: string, secret: string | BufferSource, opt: CookieOptions = {}): Promise<string> => { const signature = await makeSignature(value, secret) value = `${value}.${signature}` value = encodeURIComponent(value) return _serialize(name, value, opt)}
hono

Version Info

Tagged at
a month ago