Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix regex pattern that parses extractedValues #7

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 91 additions & 86 deletions src/js/akamai.js
Original file line number Diff line number Diff line change
@@ -1,189 +1,188 @@
function createHmac(algo, key) {
switch (algo) {
case 'sha256':
case "sha256":
return CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key);
case 'sha1':
case "sha1":
return CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, key);
case 'md5':
case "md5":
return CryptoJS.algo.HMAC.create(CryptoJS.algo.MD5, key);
}
throw new Error('HMAC algorithm should be sha256 or sha1 or md5');
throw new Error("HMAC algorithm should be sha256 or sha1 or md5");
}

class EdgeAuth {
constructor(options) {
this.options = options
this.options = options;

if (!this.options.tokenName) {
this.options.tokenName = '__token__'
this.options.tokenName = "__token__";
}

if (!this.options.key) {
throw new Error('key must be provided to generate a token.')
throw new Error("key must be provided to generate a token.");
}

if (this.options.algorithm === undefined) {
this.options.algorithm = 'sha256'
this.options.algorithm = "sha256";
}

if (this.options.escapeEarly === undefined) {
this.options.escapeEarly = false
this.options.escapeEarly = false;
}

if (!this.options.fieldDelimiter) {
this.options.fieldDelimiter = '~'
this.options.fieldDelimiter = "~";
}

if (!this.options.aclDelimiter) {
this.options.aclDelimiter = '!'
this.options.aclDelimiter = "!";
}

if (this.options.verbose === undefined) {
this.options.verbose = false
this.options.verbose = false;
}
}

_escapeEarly(text) {
if (this.options.escapeEarly) {
text = encodeURIComponent(text)
.replace(/[~'*]/g,
function (c) {
return '%' + c.charCodeAt(0).toString(16)
}
)
var pattern = /%../g
text = encodeURIComponent(text).replace(/[~'*]/g, function (c) {
return "%" + c.charCodeAt(0).toString(16);
});
var pattern = /%../g;
text = text.replace(pattern, function (match) {
return match.toLowerCase()
})
return match.toLowerCase();
});
}
return text
return text;
}

_generateToken(path, isUrl) {
var startTime = this.options.startTime
var endTime = this.options.endTime
var startTime = this.options.startTime;
var endTime = this.options.endTime;

if (typeof startTime === 'string' && startTime.toLowerCase() === 'now') {
startTime = parseInt(Date.now() / 1000)
if (typeof startTime === "string" && startTime.toLowerCase() === "now") {
startTime = parseInt(Date.now() / 1000);
} else if (startTime) {
if (typeof startTime === 'number' && startTime <= 0) {
throw new Error('startTime must be number ( > 0 ) or "now"')
if (typeof startTime === "number" && startTime <= 0) {
throw new Error('startTime must be number ( > 0 ) or "now"');
}
}

if (typeof endTime === 'number' && endTime <= 0) {
throw new Error('endTime must be number ( > 0 )')
if (typeof endTime === "number" && endTime <= 0) {
throw new Error("endTime must be number ( > 0 )");
}

if (typeof this.options.windowSeconds === 'number' && this.options.windowSeconds <= 0) {
throw new Error('windowSeconds must be number( > 0 )')
if (
typeof this.options.windowSeconds === "number" &&
this.options.windowSeconds <= 0
) {
throw new Error("windowSeconds must be number( > 0 )");
}

if (!endTime) {
if (this.options.windowSeconds) {
if (!startTime) {
startTime = parseInt(Date.now() / 1000)
startTime = parseInt(Date.now() / 1000);
}
endTime = parseInt(startTime) + parseInt(this.options.windowSeconds)
endTime = parseInt(startTime) + parseInt(this.options.windowSeconds);
} else {
throw new Error('You must provide endTime or windowSeconds')
throw new Error("You must provide endTime or windowSeconds");
}
}

if (startTime && (endTime < startTime)) {
throw new Error('Token will have already expired')
if (startTime && endTime < startTime) {
throw new Error("Token will have already expired");
}

if (this.options.verbose) {
console.log("Akamai Token Generation Parameters")
console.log("Akamai Token Generation Parameters");

if (isUrl) {
console.log(" URL : " + path)
console.log(" URL : " + path);
} else {
console.log(" ACL : " + path)
console.log(" ACL : " + path);
}

console.log(" Token Type : " + this.options.tokenType)
console.log(" Token Name : " + this.options.tokenName)
console.log(" Key/Secret : " + this.options.key)
console.log(" Algo : " + this.options.algorithm)
console.log(" Salt : " + this.options.salt)
console.log(" IP : " + this.options.ip)
console.log(" Payload : " + this.options.payload)
console.log(" Session ID : " + this.options.sessionId)
console.log(" Start Time : " + startTime)
console.log(" Window(seconds) : " + this.options.windowSeconds)
console.log(" End Time : " + endTime)
console.log(" Field Delimiter : " + this.options.fieldDelimiter)
console.log(" ACL Delimiter : " + this.options.aclDelimiter)
console.log(" Escape Early : " + this.options.escapeEarly)
}

var hashSource = []
var newToken = []
console.log(" Token Type : " + this.options.tokenType);
console.log(" Token Name : " + this.options.tokenName);
console.log(" Key/Secret : " + this.options.key);
console.log(" Algo : " + this.options.algorithm);
console.log(" Salt : " + this.options.salt);
console.log(" IP : " + this.options.ip);
console.log(" Payload : " + this.options.payload);
console.log(" Session ID : " + this.options.sessionId);
console.log(" Start Time : " + startTime);
console.log(" Window(seconds) : " + this.options.windowSeconds);
console.log(" End Time : " + endTime);
console.log(" Field Delimiter : " + this.options.fieldDelimiter);
console.log(" ACL Delimiter : " + this.options.aclDelimiter);
console.log(" Escape Early : " + this.options.escapeEarly);
}

var hashSource = [];
var newToken = [];

if (this.options.ip) {
newToken.push("ip=" + this._escapeEarly(this.options.ip))
newToken.push("ip=" + this._escapeEarly(this.options.ip));
}

if (this.options.startTime) {
newToken.push("st=" + startTime)
newToken.push("st=" + startTime);
}
newToken.push("exp=" + endTime)
newToken.push("exp=" + endTime);

if (!isUrl) {
newToken.push("acl=" + path)
newToken.push("acl=" + path);
}

if (this.options.sessionId) {
newToken.push("id=" + this._escapeEarly(this.options.sessionId))
newToken.push("id=" + this._escapeEarly(this.options.sessionId));
}

if (this.options.payload) {
newToken.push("data=" + this._escapeEarly(this.options.payload))
newToken.push("data=" + this._escapeEarly(this.options.payload));
}

hashSource = newToken.slice()
hashSource = newToken.slice();

if (isUrl) {
hashSource.push("url=" + this._escapeEarly(path))
hashSource.push("url=" + this._escapeEarly(path));
}

if (this.options.salt) {
hashSource.push("salt=" + this.options.salt)
hashSource.push("salt=" + this.options.salt);
}

this.options.algorithm = this.options.algorithm.toString().toLowerCase()
this.options.algorithm = this.options.algorithm.toString().toLowerCase();
var hmac = createHmac(
this.options.algorithm,
CryptoJS.enc.Hex.parse(this.options.key),
)
CryptoJS.enc.Hex.parse(this.options.key)
);

hmac.update(hashSource.join(this.options.fieldDelimiter))
newToken.push("hmac=" + hmac.finalize())
hmac.update(hashSource.join(this.options.fieldDelimiter));
newToken.push("hmac=" + hmac.finalize());

return newToken.join(this.options.fieldDelimiter)
return newToken.join(this.options.fieldDelimiter);
}

generateACLToken(acl) {
if (!acl) {
throw new Error('You must provide acl')
throw new Error("You must provide acl");
} else if (acl.constructor == Array) {
acl = acl.join(this.options.aclDelimiter)
acl = acl.join(this.options.aclDelimiter);
}

return this._generateToken(acl, false)
return this._generateToken(acl, false);
}

generateURLToken(url) {
if (!url) {
throw new Error('You must provide url')
throw new Error("You must provide url");
}
return this._generateToken(url, true)
return this._generateToken(url, true);
}
}


/**
* Return all values of the given response header as an Array.
*
Expand All @@ -193,8 +192,12 @@ class EdgeAuth {
*/
function getResponseHeaderValues(response, name) {
return response.headers
.filter(function (header) { return String(header.key).toLowerCase() === name; })
.map(function (header) { return header.valueOf(); });
.filter(function (header) {
return String(header.key).toLowerCase() === name;
})
.map(function (header) {
return header.valueOf();
});
}

akamai = {
Expand All @@ -203,16 +206,18 @@ akamai = {
* as an object.
*/
extractedValues(response) {
let pat = /^name=([^;]*); value=([^;]*).*$/;
return getResponseHeaderValues(response, "x-akamai-session-info")
.reduce(function (vars, value) {
let pat = /^name=(.*); value=([^\s]*)(;.*)?$/;
Copy link
Contributor

@bacalec bacalec Jun 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex in this line is assuming that there is at most 1 ";" followed by something that needs to be ignored after the value but

  • there can be 2 when variable value is extracted from qsp or from cookie. In this case variable value is not only followed by full_location_id but also by separator. In that case, the part that includes full_location_id is included in the value against which assert function is matching
  • there can be 0 when value is not extracted. In this case, if variable value includes some ";", the part of this value after the last ";" matches unepectedly with the last group.

Looking closer at groups in this regex:

  1. The 1st one (.*) is capturing but it may not need to. More importantly, it can match with any caracter including those that don't belong to a variable name. It could be more specific.
  2. The 2nd one ([^\s]*) is capturing and this is probably needed. More importantly, it excludes spaces but it shouldn't.
  3. The 3rd one (;.*)? is capturing but it may not need to. More importantly, it could be more specific.

With this regex proposed instead ^name=([^\s]*); value=(.*)(; full_location_id=[^;]*(; separator=[^;]*)?)?$:

  • it is leaving asside capturing or not
  • name and value are captured in same group 1 and 2 as before
  • group 3 that captured one of full_location_id or separator before is now capturing both if present
  • capture group 4 is added and it captures separator only

However, it requires the match to be ungreedy.

Attached screenshots show test results with U flag with PCRE2 for ungridiness, with sample values below:
proposedRegex1_ungreedy_NotExtracted

  1. name=NOT_EXTRACTED; value= key1=value1; key2=value2
  2. name=NOT_EXTRACTED; value= key3=value3

proposedRegex1_ungreedy_ExtractedFromCookieOrQsp
3. name=EXTRACTED_FROM_COOKIE_OR_QSP; value=key1=value1; key2=value2; full_location_id=cookieName3; separator=%3d
4. name=EXTRACTED_FROM_COOKIE_OR_QSP; value=key3=value3; full_location_id=cookieName3; separator=%3d

proposedRegex1_ungreedy_ExtractedFromHeader
5. name=EXTRACTED_FROM_HEADER; value=key1=value1; key2=value2; full_location_id=cookieName3
6. name=EXTRACTED_FROM_HEADER; value=key3=value3; full_location_id=cookieName3

These sample values are meant to cover:

  • variable value contains ";" or not
  • variable is not extracted
  • variable is extracted from cookie (or qsp) or is extracted from header

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a greedy match, the 1st new proposed regex capture full_location_id and separator in the group that captures value, which is not wanted.
proposedRegex1_greedy_ExtractedFromCookieOrQsp

Copy link
Contributor

@bacalec bacalec Jun 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If greediness can't be disabled, another regex similar to the one below may be used.

It it's built with an alternative where 1st option captures full_location (and eventually separator) when it's present. 2nd option should match only when full_location and separator are absent.
^name=([^\s]*); value=(.*)(; full_location_id=.*)$|^name=([^\s]*); value=(.*)$

A problem with this approach is that name and value are captured in group 1 and 2 when full_location_id is present
and in groups 4 and 5 otherwise. So this needs to be dealt with if captured groups are actually used.

proposedRegex2_greedy_NotExtracted

proposedRegex2_greedy_ExtractedFromHeader

proposedRegex2_greedy_ExtractedFromCookieOrQsp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the group that captures the value of the variable, the greedy quantifier * can be replaced by the lazy quantifier *?:
^name=([^\s]*); value=(.*?)(; full_location_id=[^;]*(; separator=[^;]*)?)?$
proposedRegex3_lazyQuantifier_ExtractedFromCookieOrQsp

return getResponseHeaderValues(response, "x-akamai-session-info").reduce(
function (vars, value) {
if (pat.test(value)) {
let [res, k, v] = value.match(pat);
vars[k] = v;
}
return vars;
}, {});
},
{}
);
},

EdgeAuth: EdgeAuth,
};
};