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

[Don't review yet] Access hostname for app's #2328

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions apps/teams-test-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ if (!urlParams.has('customInit') || !urlParams.get('customInit')) {
if (isTestBackCompat()) {
initialize(undefined, validMessageOrigins);
} else {
app.getHostName().then((hostName) => {
console.log('%cHost Name: ', 'background-color: blue', hostName);
});
console.log('App Initialization');
app.initialize(validMessageOrigins);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added a small capability which returns the host name where the app is hosted.",
"packageName": "@microsoft/teams-js",
"email": "shrshinde@microsoft.com",
"dependentChangeType": "patch"
}
48 changes: 40 additions & 8 deletions packages/teams-js/src/internal/communication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,9 @@ interface InitializeResponse {
}

/**
* @internal
* Limited to Microsoft-internal use
* set windows for communication
*/
export function initializeCommunication(
validMessageOrigins: string[] | undefined,
apiVersionTag: string,
): Promise<InitializeResponse> {
function setWindows(validMessageOrigins: string[] | undefined): void {
// Listen for messages post to our window
CommunicationPrivate.messageListener = async (evt: DOMMessageEvent): Promise<void> => await processMessage(evt);

Expand All @@ -111,10 +107,20 @@ export function initializeCommunication(
extendedWindow.onNativeMessage = handleParentMessage;
} else {
// at this point we weren't able to find a parent to talk to, no way initialization will succeed
return Promise.reject(new Error('Initialization Failed. No Parent window found.'));
throw new Error('Initialization Failed. No Parent window found.');
}
}
}

/**
* @internal
* Limited to Microsoft-internal use
*/
export function initializeCommunication(
validMessageOrigins: string[] | undefined,
apiVersionTag: string,
): Promise<InitializeResponse> {
setWindows(validMessageOrigins);
try {
// Send the initialized message to any origin, because at this point we most likely don't know the origin
// of the parent window, and this message contains no data that could pose a security risk.
Expand Down Expand Up @@ -373,6 +379,33 @@ export function sendNestedAuthRequestToTopWindow(message: string): NestedAppAuth
return sendRequestToTargetWindowHelper(targetWindow, request) as NestedAppAuthRequest;
}

/**
* @internal
* Limited to Microsoft-internal use
*/
export function sendAndGetHostName<T>(apiVersionTag: string): Promise<string | T> {
const request = [
{
id: CommunicationPrivate.nextMessageId++,
timestamp: Date.now(),
func: 'getHostName',
},
];
try {
setWindows(undefined);
Communication.parentOrigin = '*';
return sendMessageToParentAsync(apiVersionTag, 'getHostName', request);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at how this function is called in app.ts, it looks in some cases the app won't be initialized yet when you hit this line. Can I call like this actually work before the app is initialized? I think it wouldn't but maybe I'm forgetting/missing something. I think if there's no host to send it to the message will just sit forever and never receive a response.

If I assume that this won't work before initialize then I think this approach will have some problems. setWindows will not throw an error right now if the app is hosted in an iframe that isn't the hub-sdk for example.

I wonder if a slightly different approach could work. There are two different configurations TeamsJS can be in if the app that is using it is being hosted:

  1. Frameless (app is hosted inside something like a webview)
  2. Framed (app is hosted in an iframe).

For case 1, I think you could check if that one is the case relatively easily: if the current window is an ExtendedWindow that has nativeInterface defined on it, we can assume that the app is probably hosted by a host that is using a webview. Theoretically, someone that is not an official host could be running the app in a webview, but if they are it is unlikely that they have set up the WebView this way. See lines 104-107

For case 2 it is probably trickier. One thing you might be able to do is look at the domain that window.top is running at and then compare that domain to the list of validOrigins from the CDN. We may want to let the app developer pass in an additional set of "valid" host origins to check for in addition to the CDN list (the same list of valid origins they can pass to initialize).

If either of the cases above are true, we can tell the developer that they are probably being hosted by a host that will respond to their initialize call. We still might sometimes get it wrong, but in the worst case app developers will still be doing the same thing they are now: calling initialize and waiting for it to timeout.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at how this function is called in app.ts, it looks in some cases the app won't be initialized yet when you hit this line. Can I call like this actually work before the app is initialized? I think it wouldn't but maybe I'm forgetting/missing something. I think if there's no host to send it to the message will just sit forever and never receive a response.

When the app is not hosted the set windows will throw error Initialization failed. ... and we can catch that error and tell the developer that app is not hosted. In app.ts we check if GlobalVars.isInitializeCompleted then use sendAndMessageToParentAsync as this will straight give the host name. For the second part where the app is not initialized setWindows sets the windows for communication and if it doesn't find any then it just throws error, if it finds the top window we send the message which will be handled by the hubSDK.

} catch (error) {
if (error.message === 'Initialization Failed. No Parent window found.') {
return Promise.resolve('App is not running inside iframe.');
} else {
return Promise.reject(error);
}
} finally {
Communication.parentOrigin = null;
}
}

const sendRequestToTargetWindowHelperLogger = communicationLogger.extend('sendRequestToTargetWindowHelper');

/**
Expand All @@ -394,7 +427,6 @@ function sendRequestToTargetWindowHelper(
}
} else {
const targetOrigin = getTargetOrigin(targetWindow);

// If the target window isn't closed and we already know its origin, send the message right away; otherwise,
// queue the message and send it after the origin is established
if (targetWindow && targetOrigin) {
Expand Down
1 change: 1 addition & 0 deletions packages/teams-js/src/internal/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const enum ApiVersionNumber {

export const enum ApiName {
App_GetContext = 'app.getContext',
App_GetHostName = 'app.getHostName',
App_Initialize = 'app.initialize',
App_NotifyAppLoaded = 'app.notifyAppLoaded',
App_NotifyExpectedFailure = 'app.notifyExpectedFailure',
Expand Down
18 changes: 18 additions & 0 deletions packages/teams-js/src/public/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import {
Communication,
initializeCommunication,
sendAndGetHostName,
sendAndHandleStatusAndReason,
sendAndUnwrap,
sendMessageToParent,
sendMessageToParentAsync,
uninitializeCommunication,
} from '../internal/communication';
import { defaultSDKVersionForCompatCheck } from '../internal/constants';
Expand Down Expand Up @@ -731,6 +733,22 @@ export namespace app {
return GlobalVars.initializeCompleted;
}

/**
* Gets the host name where the app is running. If the app is not running in a hosted environment,
* it will return `App is not running inside iframe.` message or error message.
* @returns A promise that resolves to the host name.
*/
export async function getHostName(): Promise<string> {
if (GlobalVars.initializeCompleted) {
return await sendMessageToParentAsync(
getApiVersionTag(appTelemetryVersionNumber, ApiName.App_GetHostName),
'getHostName',
);
} else {
return await sendAndGetHostName(getApiVersionTag(appTelemetryVersionNumber, ApiName.App_GetHostName));
}
}

/**
* Gets the Frame Context that the App is running in. See {@link FrameContexts} for the list of possible values.
* @returns the Frame Context.
Expand Down
12 changes: 7 additions & 5 deletions packages/teams-js/test/internal/communication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ describe('Testing communication', () => {
GlobalVars.isFramelessWindow = false;
});

it('should throw if there is no parent window and no native interface on the current window', async () => {
it('should throw if there is no parent window and no native interface on the current window', () => {
app._initialize(undefined);
const initPromise = communication.initializeCommunication(undefined, testApiVersion);
await expect(initPromise).rejects.toThrowError('Initialization Failed. No Parent window found.');
expect(() => communication.initializeCommunication(undefined, testApiVersion)).toThrowError(
'Initialization Failed. No Parent window found.',
);
});

it('should receive valid initialize response from parent when there is no parent window but the window has a native interface', async () => {
Expand Down Expand Up @@ -206,8 +207,9 @@ describe('Testing communication', () => {
// In this case, because Communication.currentWindow is being initialized to undefined we fall back to the actual
// window object created by jest, which does not have nativeInterface defined on it
app._initialize(undefined);
const initPromise = communication.initializeCommunication(undefined, testApiVersion);
await expect(initPromise).rejects.toThrowError('Initialization Failed. No Parent window found.');
expect(() => communication.initializeCommunication(undefined, testApiVersion)).toThrowError(
'Initialization Failed. No Parent window found.',
);
});

it('should receive valid initialize response from parent when currentWindow has a parent with postMessage defined', async () => {
Expand Down
Loading