Skip to content

Commit e7e321d

Browse files
authored
Merge pull request #622 from OneSignal/fix_subdomain_for_non_prod_envs
Fixes for iframe subdomain on non-prod envs
2 parents aa38e7f + 04da6a6 commit e7e321d

File tree

4 files changed

+133
-91
lines changed

4 files changed

+133
-91
lines changed

src/context/shared/utils/Utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ export class Utils {
137137
return Number(`${majorVersion}.${minorVersion}`);
138138
}
139139

140+
/**
141+
* Gives back the last x number of parts providing a string with a delimiter.
142+
* Example: lastParts("api.staging.onesignal.com", ".", 3) will return "staging.onesignal.com"
143+
*/
144+
public static lastParts(subject: string, delimiter: string, maxParts: number): string {
145+
const parts = subject.split(delimiter);
146+
const skipParts = Math.max(parts.length - maxParts, 0);
147+
return parts.slice(skipParts).join(delimiter);
148+
}
149+
140150
/**
141151
* Checks if a version is number is greater than or equal (AKA at least) to a specific compare
142152
* to version.

src/managers/AltOriginManager.ts

Lines changed: 76 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,152 +1,148 @@
11
import { AppConfig } from '../models/AppConfig';
22
import { EnvironmentKind } from '../models/EnvironmentKind';
33
import ProxyFrameHost from '../modules/frames/ProxyFrameHost';
4-
import { contains } from '../utils';
54
import SdkEnvironment from './SdkEnvironment';
5+
import { Utils } from '../context/shared/utils/Utils';
66

77
export default class AltOriginManager {
88

99
constructor() {
1010

1111
}
1212

13-
static async discoverAltOrigin(appConfig): Promise<ProxyFrameHost> {
13+
/*
14+
* This loads all possible iframes that a site could be subscribed to
15+
* (os.tc & onesignal.com) then checks to see we are subscribed to any.
16+
* If we find what we are subscribed to both unsubscribe from onesignal.com.
17+
* This method prefers os.tc over onesignal.com where possible.
18+
*/
19+
static async discoverAltOrigin(appConfig: AppConfig): Promise<ProxyFrameHost> {
1420
const iframeUrls = AltOriginManager.getOneSignalProxyIframeUrls(appConfig);
21+
1522
const allProxyFrameHosts: ProxyFrameHost[] = [];
16-
let targetProxyFrameHost;
1723
for (const iframeUrl of iframeUrls) {
1824
const proxyFrameHost = new ProxyFrameHost(iframeUrl);
1925
// A TimeoutError could happen here; it gets rejected out of this entire loop
2026
await proxyFrameHost.load();
2127
allProxyFrameHosts.push(proxyFrameHost);
2228
}
23-
const nonDuplicatedAltOriginSubscriptions = await AltOriginManager.removeDuplicatedAltOriginSubscription(allProxyFrameHosts);
2429

25-
if (nonDuplicatedAltOriginSubscriptions) {
26-
targetProxyFrameHost = nonDuplicatedAltOriginSubscriptions[0];
30+
const subscribedProxyFrameHosts = await AltOriginManager.subscribedProxyFrameHosts(allProxyFrameHosts);
31+
await AltOriginManager.removeDuplicatedAltOriginSubscription(subscribedProxyFrameHosts);
32+
33+
let preferredProxyFrameHost: ProxyFrameHost;
34+
if (subscribedProxyFrameHosts.length === 0) {
35+
// Use the first (preferred) host (os.tc in this case) if not subscribed to any
36+
preferredProxyFrameHost = allProxyFrameHosts[0];
2737
} else {
28-
for (const proxyFrameHost of allProxyFrameHosts) {
29-
if (await proxyFrameHost.isSubscribed()) {
30-
// If we're subscribed, we're done searching for the iframe
31-
targetProxyFrameHost = proxyFrameHost;
32-
} else {
33-
if (contains(proxyFrameHost.url.host, '.os.tc')) {
34-
if (!targetProxyFrameHost) {
35-
// We've already loaded .onesignal.com and they're not subscribed
36-
// There's no other frames to check; the user is completely not subscribed
37-
targetProxyFrameHost = proxyFrameHost;
38-
} else {
39-
// Already subscribed to .onesignal.com; remove os.tc frame
40-
proxyFrameHost.dispose();
41-
}
42-
} else {
43-
// We've just loaded .onesignal.com and they're not subscribed
44-
// Load the .os.tc frame next to check
45-
// Remove the .onesignal.com frame; there's no need to keep it around anymore
46-
// Actually don't dispose it, so we can check for duplicate subscriptions
47-
proxyFrameHost.dispose();
48-
continue;
49-
}
50-
}
38+
// Otherwise if their was one or more use the highest preferred one in the list
39+
preferredProxyFrameHost = subscribedProxyFrameHosts[0];
40+
}
41+
42+
// Remove all other unneeded iframes from the page
43+
for (const proxyFrameHost of allProxyFrameHosts) {
44+
if (preferredProxyFrameHost !== proxyFrameHost) {
45+
proxyFrameHost.dispose();
5146
}
5247
}
5348

54-
return targetProxyFrameHost;
49+
return preferredProxyFrameHost;
5550
}
5651

57-
static async removeDuplicatedAltOriginSubscription(proxyFrameHosts: ProxyFrameHost[]): Promise<void | ProxyFrameHost[]> {
58-
const subscribedProxyFrameHosts = [];
52+
static async subscribedProxyFrameHosts(proxyFrameHosts: ProxyFrameHost[]): Promise<ProxyFrameHost[]> {
53+
const subscribed: ProxyFrameHost[] = [];
5954
for (const proxyFrameHost of proxyFrameHosts) {
6055
if (await proxyFrameHost.isSubscribed()) {
61-
subscribedProxyFrameHosts.push(proxyFrameHost);
56+
subscribed.push(proxyFrameHost);
6257
}
6358
}
59+
return subscribed;
60+
}
61+
62+
/*
63+
* If the user is subscribed to more than OneSignal subdomain (AKA HTTP setup)
64+
* unsubscribe them from ones lower in the list.
65+
* Example: Such as being being subscribed to mysite.os.tc & mysite.onesignal.com
66+
*/
67+
static async removeDuplicatedAltOriginSubscription(
68+
subscribedProxyFrameHosts: ProxyFrameHost[]
69+
): Promise<void> {
70+
// Not subscribed or subscribed to just one domain, nothing to do, no duplicates
6471
if (subscribedProxyFrameHosts.length < 2) {
65-
// If the user is only subscribed on one host, or not subscribed at all,
66-
// they don't have duplicate subscriptions
67-
return null;
72+
return;
6873
}
69-
if (SdkEnvironment.getBuildEnv() == EnvironmentKind.Development) {
70-
var hostToCheck = '.localhost:3001';
71-
} else if (SdkEnvironment.getBuildEnv() == EnvironmentKind.Production) {
72-
var hostToCheck = '.onesignal.com';
73-
}
74-
var oneSignalComProxyFrameHost: ProxyFrameHost = (subscribedProxyFrameHosts as any).find(proxyFrameHost => contains(proxyFrameHost.url.host, hostToCheck));
75-
if (!oneSignalComProxyFrameHost) {
76-
// They aren't subscribed to the .onesignal.com frame; shouldn't happen
77-
// unless we have 2 other frames in the future they can subscribe to
78-
return null;
79-
} else {
80-
await oneSignalComProxyFrameHost.unsubscribeFromPush();
81-
oneSignalComProxyFrameHost.dispose();
8274

83-
const indexToRemove = proxyFrameHosts.indexOf(oneSignalComProxyFrameHost);
84-
proxyFrameHosts.splice(indexToRemove, 1);
85-
return proxyFrameHosts;
75+
// At this point we have 2+ subscriptions
76+
// Keep only the first (highest priority) domain and unsubscribe the rest.
77+
const toUnsubscribeProxyFrameHosts = subscribedProxyFrameHosts.slice(1);
78+
for (const dupSubscribedHost of toUnsubscribeProxyFrameHosts) {
79+
await dupSubscribedHost.unsubscribeFromPush();
8680
}
8781
}
8882

8983
/**
84+
* Only used for sites using a OneSignal subdomain (AKA HTTP setup).
85+
*
9086
* Returns the array of possible URL in which the push subscription and
9187
* IndexedDb site data will be stored.
9288
*
93-
* For native HTTPS sites not using a subdomain of our service, this is the
94-
* top-level URL.
95-
*
96-
* For sites using a subdomain of our service, this URL was typically
97-
* subdomain.onesignal.com, until we switched to subdomain.os.tc for a shorter
98-
* origin to fit into Mac's native notifications on Chrome 59+.
89+
* This URL was typically subdomain.onesignal.com, until we switched
90+
* to subdomain.os.tc for a shorter origin to fit into Mac's native
91+
* notifications on Chrome 59+.
9992
*
10093
* Because a user may be subscribed to subdomain.onesignal.com or
10194
* subdomain.os.tc, we have to load both in certain scenarios to determine
10295
* which the user is subscribed to; hence, this method returns an array of
10396
* possible URLs.
97+
*
98+
* Order of URL is in priority order of one should be used.
10499
*/
105100
static getCanonicalSubscriptionUrls(config: AppConfig,
106101
buildEnv: EnvironmentKind = SdkEnvironment.getApiEnv()
107102
): Array<URL> {
108-
let urls = [];
103+
const subscriptionDomain = AltOriginManager.getWildcardLegacySubscriptionDomain(buildEnv);
104+
const legacyDomainUrl = new URL(`https://${config.subdomain}.${subscriptionDomain}`);
109105

110-
if (config.httpUseOneSignalCom) {
111-
let legacyDomainUrl = SdkEnvironment.getOneSignalApiUrl(buildEnv);
112-
// Add subdomain.onesignal.com
113-
legacyDomainUrl.host = [config.subdomain, legacyDomainUrl.host].join('.');
114-
urls.push(legacyDomainUrl);
106+
// Staging and Dev don't support going through the os.tc domain
107+
if (buildEnv !== EnvironmentKind.Production) {
108+
return [legacyDomainUrl];
115109
}
116110

117-
let osTcDomainUrl = SdkEnvironment.getOneSignalApiUrl(buildEnv);
118-
// Always add subdomain.os.tc
119-
osTcDomainUrl.host = [config.subdomain, 'os.tc'].join('.');
120-
urls.push(osTcDomainUrl);
111+
// Use os.tc as a first pick
112+
const urls = [new URL(`https://${config.subdomain}.os.tc`)];
121113

122-
for (const url of urls) {
123-
url.pathname = '';
114+
if (config.httpUseOneSignalCom) {
115+
urls.push(legacyDomainUrl);
124116
}
125117

126118
return urls;
127119
}
128120

129121
/**
130-
* Returns the URL of the OneSignal proxy iFrame helper.
122+
* Get the wildcard part of the legacy subscription domain.
123+
* Examples: onesignal.com, staging.onesignal.com, or localhost
131124
*/
132-
static getOneSignalProxyIframeUrls(config: AppConfig): Array<URL> {
133-
const urls = AltOriginManager.getCanonicalSubscriptionUrls(config);
134-
135-
for (const url of urls) {
136-
url.pathname = 'webPushIframe';
125+
static getWildcardLegacySubscriptionDomain(buildEnv: EnvironmentKind): string {
126+
const apiUrl = SdkEnvironment.getOneSignalApiUrl(buildEnv);
127+
128+
// Prod and Dev support domains like *.onesignal.com and *.localhost
129+
let envSubdomainParts: number = 2;
130+
if (buildEnv === EnvironmentKind.Staging) {
131+
// Allow up to 3 parts so *.staging.onesignal.com works.
132+
envSubdomainParts = 3;
137133
}
138134

139-
return urls;
135+
return Utils.lastParts(apiUrl.host, ".", envSubdomainParts);
140136
}
141137

142138
/**
143-
* Returns the URL of the OneSignal subscription popup.
139+
* Returns the URL of the OneSignal proxy iFrame helper.
144140
*/
145-
static getOneSignalSubscriptionPopupUrls(config: AppConfig): Array<URL> {
141+
static getOneSignalProxyIframeUrls(config: AppConfig): Array<URL> {
146142
const urls = AltOriginManager.getCanonicalSubscriptionUrls(config);
147143

148144
for (const url of urls) {
149-
url.pathname = 'subscribe';
145+
url.pathname = 'webPushIframe';
150146
}
151147

152148
return urls;

test/unit/managers/AltOriginManager.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,51 @@ test(`should get correct canonical subscription URL for development environment`
1212
config.httpUseOneSignalCom = true;
1313

1414
const devUrlsOsTcDomain = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Development);
15-
t.is(devUrlsOsTcDomain.length, 2);
15+
t.is(devUrlsOsTcDomain.length, 1);
1616
t.is(devUrlsOsTcDomain[0].host, new URL('https://test.localhost:3001').host);
17-
t.is(devUrlsOsTcDomain[1].host, new URL('https://test.os.tc:3001').host);
1817

1918
config.httpUseOneSignalCom = false;
2019

2120
const devUrls = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Development);
2221
t.is(devUrls.length, 1);
23-
t.is(devUrls[0].host, new URL('https://test.os.tc:3001').host);
22+
t.is(devUrls[0].host, new URL('https://test.localhost:3001').host);
2423
});
2524

2625
test(`should get correct canonical subscription URL for staging environment`, async t => {
26+
const stagingDomain = "staging.onesignal.com";
27+
(<any>global).__API_ORIGIN__ = stagingDomain;
2728
const config = TestEnvironment.getFakeAppConfig();
2829
config.subdomain = 'test';
2930
config.httpUseOneSignalCom = true;
3031

3132
const browser = await TestEnvironment.stubDomEnvironment();
32-
browser.changeURL(window, "http://staging-01.onesignal.com");
33+
browser.changeURL(window, `http://${stagingDomain}`);
3334

3435
const stagingUrlsOsTcDomain = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Staging);
35-
t.is(stagingUrlsOsTcDomain.length, 2);
36-
t.is(stagingUrlsOsTcDomain[0].host, new URL('https://test.staging-01.onesignal.com').host);
37-
t.is(stagingUrlsOsTcDomain[1].host, new URL('https://test.os.tc').host);
36+
t.is(stagingUrlsOsTcDomain.length, 1);
37+
t.is(stagingUrlsOsTcDomain[0].host, new URL(`https://test.${stagingDomain}`).host);
3838

39+
// staging does not support an alt domain so it should be the same.
3940
config.httpUseOneSignalCom = false;
40-
4141
const stagingUrls = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Staging);
4242
t.is(stagingUrls.length, 1);
43-
t.is(stagingUrls[0].host, new URL('https://test.os.tc').host);
43+
t.is(stagingUrls[0].host, new URL(`https://test.${stagingDomain}`).host);
44+
});
45+
46+
47+
test(`should get correct canonical subscription URL when api.staging.onesignal.com is used`, async t => {
48+
const stagingDomain = "staging.onesignal.com";
49+
(<any>global).__API_ORIGIN__ = `api.${stagingDomain}`;
50+
const config = TestEnvironment.getFakeAppConfig();
51+
config.subdomain = 'test';
52+
config.httpUseOneSignalCom = true;
53+
54+
const browser = await TestEnvironment.stubDomEnvironment();
55+
browser.changeURL(window, `http://${stagingDomain}`);
56+
57+
const stagingUrlsOsTcDomain = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Staging);
58+
t.is(stagingUrlsOsTcDomain.length, 1);
59+
t.is(stagingUrlsOsTcDomain[0].host, new URL(`https://test.${stagingDomain}`).host);
4460
});
4561

4662
test(`should get correct canonical subscription URL for production environment`, async t => {
@@ -50,8 +66,26 @@ test(`should get correct canonical subscription URL for production environment`,
5066

5167
const prodUrlsOsTcDomain = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Production);
5268
t.is(prodUrlsOsTcDomain.length, 2);
53-
t.is(prodUrlsOsTcDomain[0].host, new URL('https://test.onesignal.com').host);
54-
t.is(prodUrlsOsTcDomain[1].host, new URL('https://test.os.tc').host);
69+
t.is(prodUrlsOsTcDomain[0].host, new URL('https://test.os.tc').host);
70+
t.is(prodUrlsOsTcDomain[1].host, new URL('https://test.onesignal.com').host);
71+
72+
config.httpUseOneSignalCom = false;
73+
74+
const prodUrls = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Production);
75+
t.is(prodUrls.length, 1);
76+
t.is(prodUrls[0].host, new URL('https://test.os.tc').host);
77+
});
78+
79+
test(`should get correct canonical subscription URL for production environment with api. prefix`, async t => {
80+
(<any>global).__API_ORIGIN__ = `api.onesignal.com`;
81+
const config = TestEnvironment.getFakeAppConfig();
82+
config.subdomain = 'test';
83+
config.httpUseOneSignalCom = true;
84+
85+
const prodUrlsOsTcDomain = AltOriginManager.getCanonicalSubscriptionUrls(config, EnvironmentKind.Production);
86+
t.is(prodUrlsOsTcDomain.length, 2);
87+
t.is(prodUrlsOsTcDomain[0].host, new URL('https://test.os.tc').host);
88+
t.is(prodUrlsOsTcDomain[1].host, new URL('https://test.onesignal.com').host);
5589

5690
config.httpUseOneSignalCom = false;
5791

test/unit/modules/sdkEnvironment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ test('API URL should be valid for development environment', async t => {
7171
});
7272

7373
test('API URL should be valid for staging environment', async t => {
74+
const browser = await TestEnvironment.stubDomEnvironment();
75+
browser.changeURL(window, "https://localhost");
7476
const expectedUrl = `https://${window.location.host}/api/v1`;
7577
t.is(SdkEnvironment.getOneSignalApiUrl(EnvironmentKind.Staging).toString(), expectedUrl);
7678
});

0 commit comments

Comments
 (0)