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

Shim internet access out of extensions on extension host and in web view #93

Merged
merged 11 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.17.1
18.0.0
65 changes: 63 additions & 2 deletions extensions/hello-world/hello-world.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@ logger.log(
: `Successfully imported fs! fs.readFileSync = ${fs.readFileSync}`,
);

// This will be blocked and will suggest the papi.fetch api
const https = require('https');

logger.log(https.message ? https.message : `Successfully imported https!`);

try {
// This is just for testing and will throw an exception
fetch('test');
} catch (e) {
logger.log(`Hello World: Error on fetch! ${e}`);
}

try {
// This is just for testing and will throw an exception
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const xhr = new XMLHttpRequest();
} catch (e) {
logger.log(`Hello World: Error on XMLHttpRequest! ${e}`);
}

try {
// This is just for testing and will throw an exception
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const webSocket = new WebSocket();
} catch (e) {
logger.log(`Hello World: Error on WebSocket! ${e}`);
}

const unsubscribers = [];

/** Gets the code to make the Hello World React component. Provide a name to use to identify this component. Provide a string to modify the 'function HelloWorld()' line */
Expand All @@ -30,6 +58,15 @@ const getReactComponent = (name, functionModifier = '') =>
logger
} = papi;

// Test fetching
papi
.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log(scr.text.replace(/\\n/g, '')))
.catch((e) =>
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);

${functionModifier} function HelloWorld() {
const test = useContext(TestContext) || 'Context didnt work!! :(';

Expand All @@ -51,6 +88,12 @@ const getReactComponent = (name, functionModifier = '') =>
onClick: () => {
logger.log('${name} PButton clicked!');
setMyState(myStateCurrent => myStateCurrent + 1);
papi.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log('Got it! ' + scr.text.replace(/\\n/g, '')))
.catch((e) =>
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);
}
},
'Hello World PButton ',
Expand Down Expand Up @@ -89,8 +132,16 @@ exports.activate = async () => {
}),
];

papi
.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log(scr.text.replace(/\n/g, '')))
.catch((e) =>
logger.error(`Could not get Scripture from bible-api! Reason: ${e}`),
);

papi.webViews.addWebView({
hasReact: false,
contentType: 'html',
contents: `<html>
<head>
</head>
Expand Down Expand Up @@ -147,14 +198,24 @@ exports.activate = async () => {
const container = document.getElementById('root');
const root = createRoot(container);
root.render(React.createElement(HelloWorld, null));

// Test fetching
papi
.fetch('https://bible-api.com/matthew+24:14')
.then((res) => res.json())
.then((scr) => logger.log(scr.text.replace(/\\n/g, '')))
.catch((e) =>
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);
});
</script>
</body>
</html>`,
});

papi.webViews.addWebView({
contents: getReactComponent('Hello World React Webview', 'export default'),
componentName: 'HelloWorld',
contents: getReactComponent('Hello World React Webview'),
});

return Promise.all(
Expand Down
30 changes: 9 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
"rc-dock": "^3.2.17",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-frame-component": "^5.2.6",
"react-router-dom": "^6.9.0",
"ws": "^8.13.0"
},
Expand Down
42 changes: 38 additions & 4 deletions src/extension-host/services/ExtensionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getPathFromUri, joinUriPaths } from '@node/util/util';
import { Uri } from '@shared/data/FileSystemTypes';
import { UnsubscriberAsync } from '@shared/util/PapiUtil';
import Module from 'module';
import papi from '@shared/services/papi';
import papi, { MODULE_SIMILAR_APIS } from '@shared/services/papi';
import logger from '@shared/util/logger';

/** Whether this service has finished setting up */
Expand Down Expand Up @@ -142,12 +142,40 @@ const activateExtensions = async (
}

// Disallow any imports within the extension
const message = `Requiring other than papi is not allowed in extensions! Rejected require('${fileName}')`;
// TODO: make this throw so the extension knows what's going on
// Tell the extension dev if there is an api similar to what they want to import
const similarApi =
MODULE_SIMILAR_APIS[fileName] || MODULE_SIMILAR_APIS[`node:${fileName}`];
const message = `Requiring other than papi is not allowed in extensions! Rejected require('${fileName}').${
similarApi ? ` Try using papi.${similarApi}` : ''
}`;
return {
message,
};
}) as typeof Module.prototype.require;

// Shim out internet access options in environments where they are defined so extensions can't use them
const fetchOriginal: typeof fetch | undefined = globalThis.fetch;
// eslint-disable-next-line no-global-assign
globalThis.fetch = () => {
throw Error('Cannot use fetch! Try using papi.fetch');
};

const xmlHttpRequestOriginal: typeof XMLHttpRequest | undefined =
globalThis.XMLHttpRequest;
// @ts-expect-error we want to remove XMLHttpRequest
// eslint-disable-next-line no-global-assign
globalThis.XMLHttpRequest = function XMLHttpRequestForbidden() {
throw Error('Cannot use XMLHttpRequest! Try using papi.fetch');
};

const webSocketOriginal: typeof WebSocket | undefined = globalThis.WebSocket;
// @ts-expect-error we want to remove WebSocket
// eslint-disable-next-line no-global-assign
globalThis.WebSocket = function WebSocketForbidden() {
throw Error('Cannot use WebSocket!');
};

// Import the extensions and run their activate() functions
const extensionsActive = (
await Promise.all(
Expand All @@ -165,9 +193,15 @@ const activateExtensions = async (
)
).filter((activeExtension) => activeExtension !== null) as ActiveExtension[];

// Put require back so we can use it again
// TODO: this probably lets extensions wait and require code later. Security concern. Pls fix
// Put shimmed out modules and globals back so we can use them again
// TODO: replacing the original modules and globals almost confidently lets extensions wait and use them later. Serious security concern. Pls fix
Module.prototype.require = requireOriginal;
// eslint-disable-next-line no-global-assign
globalThis.fetch = fetchOriginal;
// eslint-disable-next-line no-global-assign
globalThis.XMLHttpRequest = xmlHttpRequestOriginal;
// eslint-disable-next-line no-global-assign
globalThis.WebSocket = webSocketOriginal;

return extensionsActive;
};
Expand Down
41 changes: 23 additions & 18 deletions src/renderer/components/WebView.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import { WebViewContents } from '@shared/data/WebViewTypes';
import logger from '@shared/util/logger';
import { ReactNode, useEffect, useId, useRef } from 'react';
import Frame from 'react-frame-component';
import { useEffect, useRef } from 'react';

export type WebViewProps =
| {
hasReact: false;
contents: string;
}
| {
hasReact: true;
contents: ReactNode;
};

export function WebView({ contents, hasReact = true }: WebViewProps) {
const title = useId();
export type WebViewProps = Omit<WebViewContents, 'componentName'>;

export function WebView({ contents, title, contentType }: WebViewProps) {
// This ref will always be defined
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const iframeRef = useRef<HTMLIFrameElement>(null!);
Expand Down Expand Up @@ -44,12 +34,27 @@ export function WebView({ contents, hasReact = true }: WebViewProps) {
};
}, []);

return hasReact ? (
<Frame ref={iframeRef}>{contents}</Frame>
) : (
return (
<iframe
ref={iframeRef}
title={`web-view-${title}`}
// TODO: Improve the default title when we have WebView types
title={title || `${contentType} Web View`}
// TODO: csp?
// TODO: credentialless?
// TODO: referrerpolicy?
/**
* Sandbox attribute for the webview - controls what resources scripts and other things can access.
*
* DO NOT CHANGE THIS WITHOUT A SERIOUS REASON
*/
// allow-same-origin so the iframe can get papi and communicate and such
// allow-scripts so the iframe can actually do things
// allow-pointer-lock so the iframe can lock the pointer as desired
// Note: Mozilla's iframe page 'allow-same-origin' and 'allow-scripts' warns that listing both of these
// allows the child scripts to remove this sandbox attribute from the iframe. However, it seems that this
// is done by accessing window.parent or window.top, which is removed from the iframe with the injected
// scripts in WebViewService. We will probably want to stay vigilant on security in this area.
sandbox="allow-same-origin allow-scripts allow-pointer-lock"
srcDoc={contents as string}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/docking/ErrorTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TabInfo } from '@shared/data/WebviewTypes';
import { TabInfo } from '@shared/data/WebViewTypes';

function ErrorTab({ errorMessage }: { errorMessage: string }) {
return (
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/docking/ParanextDockLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'rc-dock/dist/rc-dock.css';
import './ParanextDockLayout.css';
import { newGuid } from '@shared/util/Util';
import { SavedTabInfo, TabCreator, TabInfo } from '@shared/data/WebviewTypes';
import { SavedTabInfo, TabCreator, TabInfo } from '@shared/data/WebViewTypes';
import DockLayout, { LayoutData, TabBase, TabData, TabGroup } from 'rc-dock';
import testLayout from '@renderer/testing/testLayout';
import createHelloPanel from '@renderer/testing/HelloPanel';
Expand Down
Loading