Skip to content

Make webviews load locally #16

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

Merged
merged 2 commits into from
Nov 19, 2021
Merged

Make webviews load locally #16

merged 2 commits into from
Nov 19, 2021

Conversation

code-asher
Copy link
Member

@code-asher code-asher commented Nov 17, 2021

I also had to revert path changes (from using includes) which also fixed a couple other bugs as well (see commits).

@code-asher code-asher requested a review from jsjoeio November 17, 2021 00:46
Instead of using Microsoft's hosted endpoint.  This also fixes issues
when using code-server over http since some browsers refuse to load http
content (like when using localhost) in https contexts (since the iframe
was always https).
@@ -318,6 +348,7 @@ export class WebClientServer {
logoutEndpointUrl: this.createRequestUrl(req, parsedUrl, '/logout').toString(),
webEndpointUrl: this.createRequestUrl(req, parsedUrl, '/static').toString(),
webEndpointUrlTemplate: this.createRequestUrl(req, parsedUrl, '/static').toString(),
webviewContentExternalBaseUrlTemplate: './webview/{{uuid}}/',

updateUrl: this.createRequestUrl(req, parsedUrl, '/update/check').toString(),
Copy link
Member Author

@code-asher code-asher Nov 17, 2021

Choose a reason for hiding this comment

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

@jsjoeio For the proxy fixes it is probably sufficient to make these relative. The webview endpoints can probably just use ./ but the update and logout might be either ./ or ./../ depending on whether VS Code is being accessed from / or /vscode. We could copy the function here: https://github.com/cdr/code-server/blob/cd26f84bc6e0e6f5c17d847d9fdd718980b657a8/src/node/http.ts#L109-L121

Copy link

Choose a reason for hiding this comment

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

Nice find! Looks like that would be an easy fix to copy in

We cannot use `includes` because this causes problems with endpoints
that may share portions of their paths.

For example /webview/uuid/service-worker.js was colliding with the
/service-worker.js but moving it before caused static assets that load
from a directory called /webview to start failing.  We could move static
up as well but this may just cause other conflicts.

The standard ways of handling base paths seem to be to either ask the
user for the base path or require that a rewriting proxy be put in front
of the application (which rewrites /base/path/my/path to /my/path before
it ever gets to the application).  code-server currently requires the
latter and does not support the former but either way the code will look
similar (in the former case we would just strip the base path first,
maybe before calling `handle`).

For reference, the proxy in the Coder product is a rewriting proxy.

This also fixes issues when the web server is behind multiple Express
endpoints.  With the existing code /anything/works/ rather than just the
endpoints you specify (/vscode and / in our case) which is unexpected.
It also makes it possible to browse without a trailing slash, for
example /vscode, although that is currently broken for other reasons.
Copy link

@jsjoeio jsjoeio left a comment

Choose a reason for hiding this comment

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

Nice work! 👏 Thanks for seeing this through.

Comment on lines +534 to +535
// The webview endpoint contains variables in the format {{var}} so decode
// them as `new URI` will encode them.
Copy link

Choose a reason for hiding this comment

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

great comment 👏

Comment on lines +536 to +541
webviewContentExternalBaseUrlTemplate: decodeURIComponent(
new URL(
config.productConfiguration?.webviewContentExternalBaseUrlTemplate ?? "",
window.location.toString(), // This works without toString() but TypeScript thinks otherwise.
).toString(),
),
Copy link

Choose a reason for hiding this comment

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

❓ so here, we are decoding a new URL after constructing it with the webviewContentExternalBaseUrlTemplate and the current window.location as the base. I think

image

window.location.toString(), // This works without toString() but TypeScript thinks otherwise.

I believe window.location returns an object in some browsers but calling toString() returns the current URL, which is what we want. My guess is TS is actually correct

image

Copy link
Member Author

@code-asher code-asher Nov 17, 2021

Choose a reason for hiding this comment

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

This works in the browsers I tested:

new URL("some/path", window.location)

So TypeScript appears to be wrong about it needing to be a string. MDN tells us that any value you pass will be stringified automatically. In particular URL (and Location, it seems) will use the href property (toString() just returns href in these cases I believe).

https://developer.mozilla.org/en-US/docs/Web/API/URL/URL

Copy link

Choose a reason for hiding this comment

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

Note: The url and base arguments will each be stringified from whatever value you pass, just like with other Web APIs that accept USVString. In particular, you can use an existing URL object for either argument, and it will stringify to the object's href property.

I see what you're referring to now - TIL!

It's interesting though, according to the typedefs for new URL() it will only accept 'string | URL | undefined.

image

I wonder then if I should make an issue upstream to update the typedefs here to accept Location. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

I should also note that TypeScript does allow URL for the base but not Location for some reason. 🤷

Copy link

Choose a reason for hiding this comment

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

I should also note that TypeScript does allow URL for the base

Right! I see that in the typedefs. Weird, it doesn't allow Location. I think I'll open up an issue and see if that's a bug.

Copy link

Choose a reason for hiding this comment

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

Actually, looks like someone already did!

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh snap! Nice find.

return this._handleManifest(req, res, parsedUrl);
}

if (['favicon.ico', 'code-192.png', 'code-512.png'].includes(parsedPath.base)) {
if (pathname === '/favicon.ico' || pathname === '/manifest.json' || pathname === '/code-192.png' || pathname === '/code-512.png') {
// always serve icons/manifest, even without a token
Copy link

Choose a reason for hiding this comment

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

I guess serving icons without a token means they will always load 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, assets like these cannot use tokens otherwise they would not work on the login page.

// always serve icons/manifest, even without a token
return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', parsedPath.base));
return serveFile(this._logService, req, res, join(APP_ROOT, 'resources', 'server', pathname.substr(1)));
Copy link

Choose a reason for hiding this comment

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

❓ why do we do pathname.substr(1) here?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is from upstream. It appears to be removing the leading slash but I am not sure why. Maybe their version of join breaks when there are leading slashes?

Comment on lines +104 to +110
/**
* Add an endpoint for self-hosting webviews. This must be unique per
* webview as the code relies on unique service workers. In our case we
* use /webview/{{uuid}}.
*
* @author coder
*/
Copy link

Choose a reason for hiding this comment

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

Will this PR resolve coder/code-server#4483 then?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup!

* @author coder
*/
if (/^\/webview\//.test(pathname)) {
// always serve webview requests, even without a token
Copy link

Choose a reason for hiding this comment

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

❓ why do we always serve webview requests? wouldn't we want to only serve with the token?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question, I think you might be right. I mostly have it this way since VS Code's appears to behave this way but adding the token is probably smart.

Currently we skip the token anyway but I will make a note on the issue we have for it to block webviews once we support it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I should note that it is just serving assets so it should not be a security risk but at the same time it is only needed once you log in so might as well lock it up.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well...one more thing haha. You can actually access these files from the /static endpoint already so I guess locking it up would not achieve anything since the static endpoint does not require auth.

const relativeFilePath = normalize(normalizedPathname.split('/').splice(3).join('/'));

const filePath = join(APP_ROOT, 'out/vs/workbench/contrib/webview/browser/pre', relativeFilePath);
if (!isEqualOrParent(filePath, APP_ROOT, !isLinux)) {
Copy link

Choose a reason for hiding this comment

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

❓ do you know why we pass the !isLinux as the third argument here?

Copy link
Member Author

@code-asher code-asher Nov 17, 2021

Choose a reason for hiding this comment

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

The function signature looks like this:

function isEqualOrParent(base: string, parentCandidate: string, ignoreCase?: boolean, separator = sep): boolean

Linux is case sensitive so it looks like this is saying to be case sensitive on Linux platforms.

Copy link
Member Author

Choose a reason for hiding this comment

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

Edit: corrected, Linux is case sensitive, not insensitive. 🤦

* A unique path is required for every webview service worker.
*/
private async _handleWebview(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
const headers: Record<string, string> = Object.create(null);
Copy link

Choose a reason for hiding this comment

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

! I find the type here Record<string, string> interesting but I guess that's cause TS can't infer the correct type because we initialize as an empty object?

Copy link
Member Author

@code-asher code-asher Nov 17, 2021

Choose a reason for hiding this comment

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

Yeah I am not completely sure (this is copied from upstream) but without some kind of cast this would just be an empty object so you might run into trouble adding headers afterward, for example with:

headers["my-header"] = "my-value";

I am not sure but TypeScript might say something like "my-header is not a property of object".

I do not know why they use Record<string, string> over { [key: string]: string } though. 🤔

Copy link

Choose a reason for hiding this comment

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

Ah gotcha. That makes sense.

I do not know why they use Record<string, string> over { [key: string]: string } though. 🤔

Yeah, wish I knew where to find out why they do that 🤔

private async _handleWebview(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
const headers: Record<string, string> = Object.create(null);

// support paths that are uri-encoded (e.g. spaces => %20)
Copy link

Choose a reason for hiding this comment

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

great comment 👏

@@ -343,7 +374,7 @@ export class WebClientServer {
// the sha is the same as in src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html
`script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-cb2sg39EJV8ABaSNFfWu/ou8o1xVXYK7jp90oZ9vpcg=';`,
'child-src \'self\';',
`frame-src 'self' https://*.vscode-webview.net ${this._productService.webEndpointUrl || ''} data:;`,
`frame-src 'self' ${this._productService.webEndpointUrl || ''} data:;`,
Copy link

Choose a reason for hiding this comment

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

was this the missing puzzle piece? (I guess one of the bigger ones)

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, this is technically unnecessary but it does restrict the CSP scope which to me feels more secure.

Copy link
Member Author

@code-asher code-asher Nov 17, 2021

Choose a reason for hiding this comment

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

The really important part was setting webviewContentExternalBaseUrlTemplate so it uses the same domain you are using to access code-server and creating an endpoint for it (/webview/uuid) in order to get unique service workers per webview.

Copy link

Choose a reason for hiding this comment

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

Ah gotcha!

unique service workers per webview

Sounds like this was the main key to fix this!

@jsjoeio
Copy link

jsjoeio commented Nov 18, 2021

@code-asher anything preventing us from merging this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants