Skip to content

Commit 1174596

Browse files
authored
Merge pull request #645 from Countly/content-proxy-support
Content proxy support
2 parents 1e91ce8 + 38eec56 commit 1174596

9 files changed

Lines changed: 459 additions & 264 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 26.1.3
2+
3+
- Added support for Feedback Widgets and Content working with certain proxy configurations.
4+
15
## 26.1.2
26

37
- Delayed remote config refresh after merged device ID changes to reduce request ordering races.

cypress/e2e/bridged_utils.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function initMain(name, version) {
1515
}
1616

1717
const SDK_NAME = "javascript_native_web";
18-
const SDK_VERSION = "26.1.2";
18+
const SDK_VERSION = "26.1.3";
1919

2020
// tests
2121
describe("Bridged SDK Utilities Tests", () => {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* eslint-disable require-jsdoc */
2+
var Countly = require("../../lib/countly");
3+
var hp = require("../support/helper.js");
4+
5+
// The Cypress test server (see cypress.config.js) answers /o/sdk/content with:
6+
// { html: "http://test/_external/content?id=111&uid=tester&app_id=222", geo: {...} }
7+
// i.e. a full URL on a foreign origin/path, exactly like a server that doesn't know it is
8+
// behind a reverse proxy. #displayContent must rebase it onto the SDK's configured url.
9+
10+
describe("Content proxy path rebasing", () => {
11+
afterEach(() => {
12+
cy.task("stopServer");
13+
});
14+
15+
it("rebases the iframe src onto the SDK url and appends the encoded provided_url path", () => {
16+
hp.haltAndClearStorage(() => {
17+
cy.task("setResponseDelay", 0);
18+
cy.task("startServer");
19+
Countly.init({
20+
app_key: "YOUR_APP_KEY",
21+
url: "http://localhost:9000/countly", // proxied: Countly reached under a /countly prefix
22+
debug: true
23+
});
24+
Countly.content.enterContentZone();
25+
// enterContentZone waits ~4s after init before firing the first request
26+
cy.wait(6000).then(() => {
27+
var iframe = document.getElementById("cly-content-iframe");
28+
expect(iframe, "content iframe should be created").to.exist;
29+
// server returned http://test/_external/content?...; rebased onto this.url with
30+
// provided_url appended (path only, encoded) so the content page's assets resolve
31+
expect(iframe.getAttribute("src")).to.eq(
32+
"http://localhost:9000/countly/_external/content?id=111&uid=tester&app_id=222&provided_url=%2Fcountly"
33+
);
34+
});
35+
});
36+
});
37+
});
38+
39+
describe("parseUrlParts helper", () => {
40+
beforeEach(() => {
41+
hp.haltAndClearStorage(() => {
42+
Countly.init({ app_key: "YOUR_APP_KEY", url: "https://your.domain.count.ly", test_mode: true });
43+
});
44+
});
45+
46+
it("splits an absolute URL into origin/path/query/hash", () => {
47+
var parts = Countly._internals.parseUrlParts("http://localhost:9000/reverse-proxy/countly/feedback/rating?a=1#frag");
48+
expect(parts.origin).to.eq("http://localhost:9000");
49+
expect(parts.pathname).to.eq("/reverse-proxy/countly/feedback/rating");
50+
expect(parts.search).to.eq("?a=1");
51+
expect(parts.hash).to.eq("#frag");
52+
});
53+
54+
it("treats a scheme-less (relative) URL as an empty origin and a full path", () => {
55+
var parts = Countly._internals.parseUrlParts("/reverse-proxy/countly");
56+
expect(parts.origin).to.eq("");
57+
expect(parts.pathname).to.eq("/reverse-proxy/countly");
58+
});
59+
60+
it("returns an empty path for an origin with no path", () => {
61+
var parts = Countly._internals.parseUrlParts("https://main.server.ly");
62+
expect(parts.origin).to.eq("https://main.server.ly");
63+
expect(parts.pathname).to.eq("");
64+
});
65+
66+
it("is safe for non-string input", () => {
67+
var parts = Countly._internals.parseUrlParts(undefined);
68+
expect(parts).to.eql({ origin: "", pathname: "", search: "", hash: "" });
69+
});
70+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* eslint-disable require-jsdoc */
2+
var Countly = require("../../lib/countly");
3+
var hp = require("../support/helper.js");
4+
5+
function initWithUrl(url) {
6+
Countly.init({
7+
app_key: "YOUR_APP_KEY",
8+
url: url,
9+
test_mode: true,
10+
debug: true
11+
});
12+
}
13+
14+
var npsWidget = { _id: "widget123", type: "nps" };
15+
16+
function presentedIframeSrc() {
17+
var iframe = document.getElementById("countly-surveys-iframe");
18+
expect(iframe, "surveys iframe should be created").to.exist;
19+
return iframe.getAttribute("src");
20+
}
21+
22+
describe("Feedback widget provided_url parameter", () => {
23+
it("sends the path prefix (path only, encoded) when the SDK url has one", () => {
24+
hp.haltAndClearStorage(() => {
25+
initWithUrl("https://your.domain.count.ly/reverse-proxy/countly");
26+
Countly.present_feedback_widget(npsWidget);
27+
var src = presentedIframeSrc();
28+
// Server rejects any value with ":" or "//", so we send the pathname only.
29+
expect(src).to.include("provided_url=" + encodeURIComponent("/reverse-proxy/countly"));
30+
// The origin/scheme must never leak into the value.
31+
expect(src).to.not.include("provided_url=https");
32+
});
33+
});
34+
35+
it("resolves the path prefix for a same-origin (relative) SDK url", () => {
36+
hp.haltAndClearStorage(() => {
37+
initWithUrl("/reverse-proxy/countly");
38+
Countly.present_feedback_widget(npsWidget);
39+
var src = presentedIframeSrc();
40+
expect(src).to.include("provided_url=" + encodeURIComponent("/reverse-proxy/countly"));
41+
});
42+
});
43+
44+
it("omits provided_url entirely when the SDK url has no path prefix", () => {
45+
hp.haltAndClearStorage(() => {
46+
initWithUrl("https://your.domain.count.ly");
47+
Countly.present_feedback_widget(npsWidget);
48+
var src = presentedIframeSrc();
49+
expect(src).to.not.include("provided_url");
50+
});
51+
});
52+
});

cypress/e2e/sdk_info.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
var Countly = require("../../lib/countly");
44
var hp = require("../support/helper.js");
55
const SDK_NAME = "javascript_native_web";
6-
const SDK_VERSION = "26.1.2";
6+
const SDK_VERSION = "26.1.3";
77

88
function initMain(name, version) {
99
Countly.init({

lib/countly.js

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@
350350
backoffCount: "cly_hc_backoff_count",
351351
consecutiveBackoffCount: "cly_hc_consecutive_backoff_count"
352352
});
353-
var SDK_VERSION = "26.1.2";
353+
var SDK_VERSION = "26.1.3";
354354
var SDK_NAME = "javascript_native_web";
355355

356356
// Using this on document.referrer would return an array with 17 elements in it. The 12th element (array[11]) would be the path we are looking for. Others would be things like password and such (use https://regex101.com/ to check more)
@@ -1092,6 +1092,39 @@
10921092
}
10931093
}
10941094

1095+
/**
1096+
* Split a URL string into its parts using the shared urlParseRE regex. Kept regex-based
1097+
* (rather than the URL constructor) to preserve IE11 support, and it needs no DOM access.
1098+
* Relative URLs (no scheme/host) return an empty origin and everything in pathname.
1099+
* @memberof Countly._internals
1100+
* @param {String} urlString - URL to parse
1101+
* @returns {Object} object with origin, pathname, search and hash string properties
1102+
*/
1103+
function parseUrlParts(urlString) {
1104+
var parts = {
1105+
origin: "",
1106+
pathname: "",
1107+
search: "",
1108+
hash: ""
1109+
};
1110+
if (typeof urlString !== "string") {
1111+
return parts;
1112+
}
1113+
var matches = urlParseRE.exec(urlString);
1114+
if (!matches) {
1115+
return parts;
1116+
}
1117+
// matches[4] = "protocol:", matches[10] = "host[:port]", matches[13] = path,
1118+
// matches[16] = "?query", matches[17] = "#hash" (see urlParseRE comments in Constants.js)
1119+
if (matches[4] && matches[10]) {
1120+
parts.origin = matches[4] + "//" + matches[10];
1121+
}
1122+
parts.pathname = matches[13] || "";
1123+
parts.search = matches[16] || "";
1124+
parts.hash = matches[17] || "";
1125+
return parts;
1126+
}
1127+
10951128
var _consentTimer = /*#__PURE__*/new WeakMap();
10961129
var _self = /*#__PURE__*/new WeakMap();
10971130
var _global = /*#__PURE__*/new WeakMap();
@@ -5104,6 +5137,15 @@
51045137
// Origin is passed to the popup so that it passes it back in the postMessage event
51055138
// Only web SDK passes origin and web
51065139
url += "&origin=" + passedOrigin;
5140+
// Pass the path prefix (if any) for reverse proxy scenarios. The server only accepts a
5141+
// same-origin relative path, so we send just the pathname and omit it when there is none.
5142+
var providedPath = "";
5143+
if (_this.url) {
5144+
providedPath = stripTrailingSlash(parseUrlParts(_this.url).pathname);
5145+
}
5146+
if (providedPath) {
5147+
url += "&provided_url=" + encodeURIComponent(providedPath);
5148+
}
51075149
url += "&widget_v=web";
51085150
var iframe = document.createElement("iframe");
51095151
iframe.src = url;
@@ -5714,13 +5756,31 @@
57145756
});
57155757
_classPrivateFieldInitSpec(this, _displayContent, function (response) {
57165758
try {
5759+
// 1. rebase html onto dev provided url so the content page loads from the right path
5760+
// 2. pass the SDK path prefix as provided_url
5761+
var contentUrl = response.html;
5762+
if (typeof contentUrl === "string") {
5763+
var parts = parseUrlParts(contentUrl);
5764+
var base = parts.origin + parts.pathname;
5765+
if (_this.url && contentUrl.indexOf(_this.url) !== 0) {
5766+
base = stripTrailingSlash(_this.url) + parts.pathname;
5767+
}
5768+
var search = parts.search;
5769+
var providedPath = _this.url ? stripTrailingSlash(parseUrlParts(_this.url).pathname) : "";
5770+
if (providedPath) {
5771+
// insert into the query (before any #hash); server ignores values with ':' or '//'
5772+
search += (search ? "&" : "?") + "provided_url=" + encodeURIComponent(providedPath);
5773+
}
5774+
contentUrl = base + search + parts.hash;
5775+
_classPrivateFieldGet2(_log, _this).call(_this, logLevelEnums.DEBUG, "displayContent, Content iframe URL:[" + contentUrl + "]");
5776+
}
57175777
var iframe = document.createElement("iframe");
57185778
iframe.id = _classPrivateFieldGet2(_contentIframeID, _this);
57195779
// always https in the future
57205780
// response.html = response.html.replace(/http:\/\//g, "https://");
5721-
iframe.src = response.html;
5781+
iframe.src = contentUrl;
57225782
iframe.style.position = "absolute";
5723-
if (response.html.indexOf("feedback/survey") != -1) {
5783+
if (contentUrl.indexOf("feedback/survey") != -1) {
57245784
// for surveys to scroll with the page (nps is not in journeys yet)
57255785
iframe.style.position = "fixed";
57265786
}
@@ -5742,7 +5802,13 @@
57425802
}
57435803
});
57445804
_classPrivateFieldInitSpec(this, _interpretContentMessage, function (messageEvent) {
5745-
if (_this.contentWhitelist.indexOf(messageEvent.origin) === -1) {
5805+
// messageEvent.origin is always origin-only (scheme://host[:port]), while whitelist
5806+
// entries (including this.url) may carry a path prefix behind a reverse proxy. Compare
5807+
// by origin so proxied content (served from this.url's origin) is not rejected.
5808+
var isWhitelistedOrigin = Array.isArray(_this.contentWhitelist) && _this.contentWhitelist.some(function (entry) {
5809+
return parseUrlParts(entry).origin === messageEvent.origin;
5810+
});
5811+
if (!isWhitelistedOrigin) {
57465812
// this.#log(logLevelEnums.ERROR, "interpretContentMessage, Received message from invalid origin");
57475813
// silent ignore
57485814
return;
@@ -7417,6 +7483,7 @@
74177483
truncateObject: truncateObject,
74187484
truncateSingleValue: truncateSingleValue,
74197485
stripTrailingSlash: stripTrailingSlash,
7486+
parseUrlParts: parseUrlParts,
74207487
prepareParams: prepareParams,
74217488
sendXmlHttpRequest: _classPrivateFieldGet2(_sendXmlHttpRequest, this),
74227489
isResponseValid: _classPrivateFieldGet2(_isResponseValid, this),
@@ -7889,4 +7956,4 @@
78897956

78907957
Object.defineProperty(exports, '__esModule', { value: true });
78917958

7892-
}));
7959+
}));

0 commit comments

Comments
 (0)