return lines;
};
+const validTokenRE = /^[!#$%&'*+-.0-9A-Z^_`a-z~]+$/;
+const validValueRE = /^[!#$%&'()*+-./0-9:<=>?@A-Z[\]^_`a-z{|}~]*$/;
+const validPathRE = /^[ !"#$%&'()*+,-./0-9:<=>?@A-Z[\\\]^_`a-z{|}~]*$/;
+const validLabelRE = /^[a-zA-Z0-9-]+$/;
+const invalidLabelRE = /--|^-|-$/;
+
/**
* Adds a new set-cookie header value to response, with supplied data.
* @param {http.ServerResponse} res response
* @param {string=} opt.path cookie path
* @param {string=} opt.sameSite cookie sharing
* @param {boolean=} opt.secure cookie security
+ * @param {string[]=} opt.extension cookie extension attribute values
*/
function addCookie(res, name, value, opt = {}) {
const options = {
path: undefined,
sameSite: undefined,
secure: false,
+ extension: [],
...opt,
};
- // TODO: validate name, value
+
+ if (!validTokenRE.test(name)) {
+ throw new RangeError('invalid cookie name');
+ }
+
+ if (value.startsWith('"') && value.endsWith('"')) {
+ if (!validValueRE.test(value.slice(1, value.length - 1))) {
+ throw new RangeError('invalid cookie value');
+ };
+ } else if (!validValueRE.test(value)) {
+ throw new RangeError('invalid cookie value');
+ }
+
const cookieParts = [
`${name}=${value}`,
];
+
if (options.domain) {
+ for (const label of options.domain.split('.')) {
+ if (!validLabelRE.test(label) || invalidLabelRE.test(label)) {
+ throw new RangeError('invalid cookie domain');
+ }
+
+ }
cookieParts.push(`Domain=${options.domain}`);
}
+
if (options.expires) {
if (!(options.expires instanceof Date)) {
throw new TypeError('cookie expires must be Date');
}
cookieParts.push(`Expires=${options.expires.toUTCString()}`);
}
+
if (options.httpOnly) {
cookieParts.push('HttpOnly');
}
+
if (options.maxAge) {
cookieParts.push(`Max-Age=${options.maxAge}`);
}
+
if (options.path) {
+ if (!validPathRE.test(options.path)) {
+ throw new RangeError('cookie path value not valid');
+ }
cookieParts.push(`Path=${options.path}`);
}
+
if (options.sameSite) {
if (!(['Strict', 'Lax', 'None'].includes(options.sameSite))) {
throw new RangeError('cookie sameSite value not valid');
}
+ if (options.sameSite === 'None'
+ && !options.secure) {
+ throw new RangeError('cookie with sameSite None must also be secure');
+ }
cookieParts.push(`SameSite=${options.sameSite}`);
}
+
if (options.secure) {
cookieParts.push('Secure');
}
+
+ if (!Array.isArray(options.extension)) {
+ throw new TypeError('cookie extension must be Array');
+ }
+ for (const extension of options.extension) {
+ if (!validPathRE.test(extension)) {
+ throw new RangeError('cookie extension value not valid');
+ }
+ cookieParts.push(extension);
+ }
+
res.appendHeader(Enum.Header.SetCookie, cookieParts.join('; '));
}
path: '/foo',
sameSite: 'Lax',
secure: true,
+ extension: ['Mischief'],
});
assert(res.appendHeader.called);
});
assert.throws(() => common.addCookie(res, name, value, { expires: 'never' }), TypeError);
});
it('covers invalid sameSite', function () {
- assert.throws(() => common.addCookie(res, name, value, { sameSite: 'Whatever' }));
+ assert.throws(() => common.addCookie(res, name, value, { sameSite: 'Whatever' }), RangeError);
+ });
+ it('covers invalid sameSite/secure setting', function () {
+ assert.throws(() => common.addCookie(res, name, value, { sameSite: 'None', secure: false }), RangeError);
+ });
+ it('covers invalid path', function () {
+ assert.throws(() => common.addCookie(res, name, value, { path: '/bad;path' }), RangeError);
+ });
+ it('covers invalid domain', function () {
+ assert.throws(() => common.addCookie(res, name, value, { domain: 'a-.com' }), RangeError);
+ });
+ it('covers invalid extension type', function () {
+ assert.throws(() => common.addCookie(res, name, value, { extension: 'extension' }), TypeError);
+ });
+ it('covers invalid extension', function () {
+ assert.throws(() => common.addCookie(res, name, value, { extension: ['bad;extension'] }), RangeError);
+ });
+ it('covers invalid name', function () {
+ name = 'bad:name';
+ assert.throws(() => common.addCookie(res, name, value), RangeError);
+ });
+ it('covers invalid value', function () {
+ value = 'bad;value';
+ assert.throws(() => common.addCookie(res, name, value), RangeError);
+ });
+ it('covers quoted value', function () {
+ value = '"value"';
+ common.addCookie(res, name, value);
+ assert(res.appendHeader.called);
+ });
+ it('covers invalid quoted value', function () {
+ value = '"bad;value"';
+ assert.throws(() => common.addCookie(res, name, value), RangeError);
});
it('covers no options', function () {
common.addCookie(res, name, value);