Merge branch 'v2.1-dev' as v2.1.2
[squeep-api-dingus] / lib / common.js
index c6c21efc705cf7581dfd351ffe8ba3a8275cbdb1..2f0aeb835694b5eb0f5e6130b8974ff645c585b6 100644 (file)
@@ -205,6 +205,12 @@ const unfoldHeaderLines = (lines) => {
   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
@@ -219,6 +225,7 @@ const unfoldHeaderLines = (lines) => {
  * @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 = {
@@ -229,39 +236,83 @@ function addCookie(res, name, value, opt = {}) {
     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('; '));
 }