| 
 | 1 | +import type { IncomingMessage, ServerResponse } from 'node:http'  | 
 | 2 | +import type { CookiesOption, GetCookieOption, SetCookieOption } from './types'  | 
 | 3 | +import http from 'node:http'  | 
 | 4 | +import { isArray, toArray } from '@pengzhanbo/utils'  | 
 | 5 | +import { REGEXP_CACHE, REGEXP_ESCAPE_CHARS_REGEXP } from './constants'  | 
 | 6 | +import { Cookie } from './Cookie'  | 
 | 7 | +import { Keygrip } from './Keygrip'  | 
 | 8 | + | 
 | 9 | +export class Cookies {  | 
 | 10 | +  request: IncomingMessage  | 
 | 11 | +  response: ServerResponse<IncomingMessage>  | 
 | 12 | +  secure: boolean | undefined  | 
 | 13 | +  keys: Keygrip | undefined  | 
 | 14 | + | 
 | 15 | +  constructor(  | 
 | 16 | +    req: IncomingMessage,  | 
 | 17 | +    res: ServerResponse<IncomingMessage>,  | 
 | 18 | +    options: CookiesOption = {},  | 
 | 19 | +  ) {  | 
 | 20 | +    this.request = req  | 
 | 21 | +    this.response = res  | 
 | 22 | +    this.secure = options.secure  | 
 | 23 | + | 
 | 24 | +    if (options.keys instanceof Keygrip) {  | 
 | 25 | +      this.keys = options.keys  | 
 | 26 | +    }  | 
 | 27 | +    else if (isArray(options.keys)) {  | 
 | 28 | +      this.keys = new Keygrip(options.keys)  | 
 | 29 | +    }  | 
 | 30 | +  }  | 
 | 31 | + | 
 | 32 | +  set(name: string, value?: string | null, options?: SetCookieOption): this {  | 
 | 33 | +    const req = this.request  | 
 | 34 | +    const res = this.response  | 
 | 35 | +    const headers = toArray(res.getHeader('Set-Cookie')) as string[]  | 
 | 36 | +    const cookie = new Cookie(name, value, options)  | 
 | 37 | +    const signed = options?.signed ?? !!this.keys  | 
 | 38 | +    const secure = this.secure === undefined  | 
 | 39 | +      ? (req as IncomingMessage & { protocol: string }).protocol === 'https' || isRequestEncrypted(req)  | 
 | 40 | +      : Boolean(this.secure)  | 
 | 41 | + | 
 | 42 | +    if (!secure && options?.secure) {  | 
 | 43 | +      throw new Error('Cannot send secure cookie over unencrypted connection')  | 
 | 44 | +    }  | 
 | 45 | + | 
 | 46 | +    cookie.secure = options?.secure ?? secure  | 
 | 47 | + | 
 | 48 | +    pushCookie(headers, cookie)  | 
 | 49 | + | 
 | 50 | +    if (signed && options) {  | 
 | 51 | +      if (!this.keys)  | 
 | 52 | +        throw new Error('.keys required for signed cookies')  | 
 | 53 | +      cookie.value = this.keys.sign(cookie.toString())  | 
 | 54 | +      cookie.name += '.sig'  | 
 | 55 | +      pushCookie(headers, cookie)  | 
 | 56 | +    }  | 
 | 57 | + | 
 | 58 | +    const setHeader = (res as any).set ? http.OutgoingMessage.prototype.setHeader : res.setHeader  | 
 | 59 | +    setHeader.call(res, 'Set-Cookie', headers)  | 
 | 60 | + | 
 | 61 | +    return this  | 
 | 62 | +  }  | 
 | 63 | + | 
 | 64 | +  get(name: string, options?: GetCookieOption): string | void {  | 
 | 65 | +    const signName = `${name}.sig`  | 
 | 66 | +    const signed = options?.signed ?? !!this.keys  | 
 | 67 | + | 
 | 68 | +    const header = this.request.headers.cookie  | 
 | 69 | + | 
 | 70 | +    if (!header)  | 
 | 71 | +      return  | 
 | 72 | + | 
 | 73 | +    const match = header.match(getPattern(name))  | 
 | 74 | + | 
 | 75 | +    if (!match)  | 
 | 76 | +      return  | 
 | 77 | + | 
 | 78 | +    let value = match[1]  | 
 | 79 | + | 
 | 80 | +    if (value[0] === '"')  | 
 | 81 | +      value = value.slice(1, -1)  | 
 | 82 | + | 
 | 83 | +    if (!options || !signed)  | 
 | 84 | +      return value  | 
 | 85 | + | 
 | 86 | +    const remote = this.get(signName)  | 
 | 87 | +    if (!remote)  | 
 | 88 | +      return  | 
 | 89 | + | 
 | 90 | +    const data = `${name}=${value}`  | 
 | 91 | + | 
 | 92 | +    if (!this.keys)  | 
 | 93 | +      throw new Error('.keys required for signed cookies')  | 
 | 94 | + | 
 | 95 | +    const index = this.keys.index(data, remote)  | 
 | 96 | +    if (index < 0) {  | 
 | 97 | +      this.set(signName, null, { path: '/', signed: false })  | 
 | 98 | +    }  | 
 | 99 | +    else {  | 
 | 100 | +      index && this.set(signName, this.keys.sign(data), { signed: false })  | 
 | 101 | +      return value  | 
 | 102 | +    }  | 
 | 103 | +  }  | 
 | 104 | +}  | 
 | 105 | + | 
 | 106 | +/**  | 
 | 107 | + * Get the pattern to search for a cookie in a string.  | 
 | 108 | + */  | 
 | 109 | +function getPattern(name: string): RegExp {  | 
 | 110 | +  if (!REGEXP_CACHE[name]) {  | 
 | 111 | +    REGEXP_CACHE[name] = new RegExp(  | 
 | 112 | +      `(?:^|;) *${  | 
 | 113 | +        name.replace(REGEXP_ESCAPE_CHARS_REGEXP, '\\$&')  | 
 | 114 | +      }=([^;]*)`,  | 
 | 115 | +    )  | 
 | 116 | +  }  | 
 | 117 | + | 
 | 118 | +  return REGEXP_CACHE[name]  | 
 | 119 | +}  | 
 | 120 | + | 
 | 121 | +/**  | 
 | 122 | + * Get the encrypted status for a request.  | 
 | 123 | + */  | 
 | 124 | +function isRequestEncrypted(req: IncomingMessage): boolean {  | 
 | 125 | +  return Boolean(req.socket  | 
 | 126 | +    ? (req.socket as any).encrypted  | 
 | 127 | +    : (req.connection as any).encrypted)  | 
 | 128 | +}  | 
 | 129 | + | 
 | 130 | +function pushCookie(headers: string[], cookie: Cookie): void {  | 
 | 131 | +  if (cookie.overwrite) {  | 
 | 132 | +    for (let i = headers.length - 1; i >= 0; i--) {  | 
 | 133 | +      if (headers[i].indexOf(`${cookie.name}=`) === 0) {  | 
 | 134 | +        headers.splice(i, 1)  | 
 | 135 | +      }  | 
 | 136 | +    }  | 
 | 137 | +  }  | 
 | 138 | + | 
 | 139 | +  headers.push(cookie.toHeader())  | 
 | 140 | +}  | 
0 commit comments