mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 17:47:22 +00:00
350 lines
10 KiB
TypeScript
350 lines
10 KiB
TypeScript
/**********************************************************************
|
|
* Copyright (C) 2024 Red Hat, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
***********************************************************************/
|
|
|
|
import { existsSync } from 'node:fs';
|
|
import type * as http from 'node:http';
|
|
import { resolve } from 'node:path';
|
|
|
|
import type * as podmanDesktopAPI from '@podman-desktop/api';
|
|
import type { Application } from 'express';
|
|
import express from 'express';
|
|
import { inject, injectable } from 'inversify';
|
|
|
|
import { ApiSenderType } from '/@/plugin/api.js';
|
|
import { Uri } from '/@/plugin/types/uri.js';
|
|
import type { WebviewInfo, WebviewSimpleInfo } from '/@api/webview-info.js';
|
|
|
|
import { getFreePort } from '../util/port.js';
|
|
import { DevToolsManager } from './devtools-manager.js';
|
|
import { WebviewImpl } from './webview-impl.js';
|
|
import { WebviewPanelImpl } from './webview-panel-impl.js';
|
|
|
|
type IconPath = Uri | { readonly light: Uri; readonly dark: Uri };
|
|
|
|
export class HttpServer {
|
|
#app: Application;
|
|
|
|
#instance: http.Server | undefined;
|
|
|
|
constructor(app: Application) {
|
|
this.#app = app;
|
|
this.config(app);
|
|
}
|
|
|
|
private config(_app: Application): void {}
|
|
|
|
async start(serverPort: number): Promise<void> {
|
|
console.log('Starting http server to handle webviews on port', serverPort);
|
|
// now listen on the port
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.#instance = this.#app
|
|
.listen(serverPort, () => {
|
|
resolve();
|
|
})
|
|
.on('error', (err: unknown) => {
|
|
reject(new Error(String(err)));
|
|
});
|
|
});
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
if (!this.#instance) {
|
|
return;
|
|
}
|
|
return new Promise<void>((resolve, reject) => {
|
|
this.#instance?.close((err: unknown) => {
|
|
if (err) {
|
|
reject(new Error(String(err)));
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
@injectable()
|
|
export class WebviewRegistry {
|
|
#count = 0;
|
|
#webviews: Map<string, WebviewPanelImpl>;
|
|
|
|
#apiSender: ApiSenderType;
|
|
|
|
// express server instance for serving webviews
|
|
#expressServer: HttpServer;
|
|
|
|
#uuidAndPaths: Map<string, string>;
|
|
|
|
#serverPort: number = 0;
|
|
|
|
#app: Application;
|
|
|
|
constructor(
|
|
@inject(ApiSenderType) apiSender: ApiSenderType,
|
|
@inject(DevToolsManager) private devToolsManager: DevToolsManager,
|
|
) {
|
|
this.#apiSender = apiSender;
|
|
this.#webviews = new Map();
|
|
this.#uuidAndPaths = new Map();
|
|
|
|
this.#app = express();
|
|
this.#expressServer = new HttpServer(this.#app);
|
|
}
|
|
|
|
protected initRouting(): void {
|
|
const router = this.buildRouter();
|
|
this.configureRouter(router);
|
|
this.useAppRouter(router);
|
|
}
|
|
|
|
protected useAppRouter(router: express.Router): void {
|
|
this.#app.use('/', router);
|
|
}
|
|
|
|
protected configureRouter(router: express.Router): void {
|
|
router.get('/*', (req, res) => {
|
|
// no referrer, reject request
|
|
if (!req.headers.referer) {
|
|
res.status(500).send('invalid request');
|
|
return;
|
|
}
|
|
|
|
// return empty page in case of root path access
|
|
if (req.path === '/') {
|
|
res.send('<html></html>');
|
|
return;
|
|
}
|
|
|
|
// get uuid of the request using the host
|
|
// extract 58b7cdef-294f-4ace-93cc-de6e0a139592 from req.hostname 58b7cdef-294f-4ace-93cc-de6e0a139592.webview.localhost
|
|
// check if hostname matches uuid pattern
|
|
const uuidPattern = new RegExp('^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$');
|
|
const uuid = req.hostname.split('.')[0];
|
|
if (!uuid) {
|
|
res.status(500).send('invalid request: uuid');
|
|
return;
|
|
}
|
|
if (!uuidPattern.test(uuid)) {
|
|
res.status(404).send('not found');
|
|
return;
|
|
}
|
|
|
|
// check if webview with uuid exists
|
|
const rootExtensionPath = this.#uuidAndPaths.get(uuid);
|
|
|
|
if (!rootExtensionPath) {
|
|
// throw an error (500)
|
|
res.status(500).send('missing parameter');
|
|
return;
|
|
}
|
|
|
|
// construct path from root path and req.path
|
|
const fullPath = `${rootExtensionPath}/${req.path}`;
|
|
|
|
// make it absolute
|
|
const absolutePath = resolve(fullPath);
|
|
|
|
// root path in absolute form
|
|
const rootExtensionPathAbsolute = resolve(rootExtensionPath);
|
|
|
|
// check that path is subfolder of root path
|
|
if (!absolutePath.startsWith(rootExtensionPathAbsolute)) {
|
|
res.status(404).send('not found');
|
|
return;
|
|
}
|
|
|
|
// check that path exists on the filesystem
|
|
if (!existsSync(absolutePath)) {
|
|
res.status(404).send('not found');
|
|
return;
|
|
}
|
|
|
|
// send content of absolute path with express
|
|
res.sendFile(absolutePath);
|
|
});
|
|
}
|
|
|
|
protected buildRouter(): express.Router {
|
|
return express.Router({
|
|
strict: true,
|
|
});
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
// init routing
|
|
this.initRouting();
|
|
|
|
// grab a free port
|
|
this.#serverPort = await getFreePort(44000);
|
|
|
|
// start the express server
|
|
await this.#expressServer.start(this.#serverPort);
|
|
}
|
|
|
|
async makeDefaultWebviewVisible(id: string): Promise<void> {
|
|
// filter all webviews matching the given id
|
|
const activeWebviewPanels = Array.from(this.#webviews.values()).filter(webviewPanel => {
|
|
return webviewPanel.internalId === id;
|
|
});
|
|
|
|
// update the state of the webviews
|
|
for (const webviewPanel of activeWebviewPanels) {
|
|
webviewPanel.updateViewState(true, true);
|
|
}
|
|
|
|
// now, update the state of the non active webviews
|
|
const nonActiveWebviewPanels = Array.from(this.#webviews.values()).filter(webviewPanel => {
|
|
return webviewPanel.internalId !== id;
|
|
});
|
|
for (const webviewPanel of nonActiveWebviewPanels) {
|
|
webviewPanel.updateViewState(false, false);
|
|
}
|
|
}
|
|
|
|
// stop the express server
|
|
async stop(): Promise<void> {
|
|
await this.#expressServer.stop();
|
|
}
|
|
|
|
createWebviewPanel(
|
|
extensionInfo: { id: string; extensionPath: string; icon?: string | { light: string; dark: string } },
|
|
viewType: string,
|
|
title: string,
|
|
options?: podmanDesktopAPI.WebviewOptions,
|
|
): podmanDesktopAPI.WebviewPanel {
|
|
const id = `${this.#count}`;
|
|
this.#count++;
|
|
|
|
// default icon is the extension icon
|
|
let iconPath: IconPath | undefined = undefined;
|
|
if (extensionInfo.icon) {
|
|
if (typeof extensionInfo.icon === 'string') {
|
|
iconPath = Uri.file(extensionInfo.icon);
|
|
} else {
|
|
iconPath = {
|
|
light: Uri.file(extensionInfo.icon.light),
|
|
dark: Uri.file(extensionInfo.icon.dark),
|
|
};
|
|
}
|
|
}
|
|
|
|
const webview: WebviewImpl = new WebviewImpl(
|
|
viewType,
|
|
id,
|
|
this.#apiSender,
|
|
extensionInfo,
|
|
this.#serverPort,
|
|
options ?? {},
|
|
);
|
|
|
|
const webviewPanelImpl = new WebviewPanelImpl(id, this, this.#apiSender, webview, {
|
|
title,
|
|
iconPath,
|
|
webviewOptions: options,
|
|
viewType,
|
|
});
|
|
|
|
this.#uuidAndPaths.set(webview.uuid, extensionInfo.extensionPath);
|
|
this.#webviews.set(webviewPanelImpl.internalId, webviewPanelImpl);
|
|
this.#apiSender.send('webview-create', webviewPanelImpl.internalId);
|
|
return webviewPanelImpl;
|
|
}
|
|
|
|
disposeWebviewPanel(webviewPanelImpl: WebviewPanelImpl): void {
|
|
this.#webviews.delete(webviewPanelImpl.internalId);
|
|
this.#apiSender.send('webview-delete', webviewPanelImpl.internalId);
|
|
}
|
|
|
|
async postMessageToWebview(id: string, message: { data: unknown }): Promise<void> {
|
|
/// grab the webview
|
|
const webviewPanelImpl = this.#webviews.get(id);
|
|
|
|
await webviewPanelImpl?.webview.handleMessage(message.data);
|
|
}
|
|
|
|
async updateWebviewState(id: string, state: unknown): Promise<void> {
|
|
/// grab the webview
|
|
const webviewPanelImpl = this.#webviews.get(id);
|
|
|
|
if (webviewPanelImpl) {
|
|
// update state
|
|
webviewPanelImpl.webview.state = state;
|
|
}
|
|
}
|
|
|
|
getRegistryHttpPort(): number {
|
|
return this.#serverPort;
|
|
}
|
|
|
|
/**
|
|
* Register DevTools for a WebView when they are opened.
|
|
* This method is called via IPC from the renderer process.
|
|
*/
|
|
async registerWebviewDevTools(webcontentId: number): Promise<void> {
|
|
return this.devToolsManager.registerDevTools(webcontentId);
|
|
}
|
|
|
|
/**
|
|
* Clean up DevTools when a WebView is being destroyed.
|
|
* This method is called via IPC from the renderer process.
|
|
*/
|
|
async cleanupWebviewDevTools(webcontentId: number): Promise<void> {
|
|
return this.devToolsManager.cleanupDevTools(webcontentId);
|
|
}
|
|
|
|
listWebviews(): WebviewInfo[] {
|
|
return Array.from(this.#webviews.entries()).map(entry => {
|
|
const id = entry[0];
|
|
const webviewPanelImpl = entry[1];
|
|
const viewType: string = webviewPanelImpl.viewType;
|
|
const sourcePath = webviewPanelImpl.webview.extensionInfo.extensionPath;
|
|
const html = webviewPanelImpl.webview.html;
|
|
const state = webviewPanelImpl.webview.state;
|
|
|
|
let icon;
|
|
if (webviewPanelImpl.iconPath instanceof Uri) {
|
|
icon = webviewPanelImpl.iconPath.fsPath;
|
|
}
|
|
|
|
return {
|
|
id,
|
|
viewType,
|
|
sourcePath,
|
|
html,
|
|
icon,
|
|
name: webviewPanelImpl.title,
|
|
uuid: webviewPanelImpl.webview.uuid,
|
|
state,
|
|
};
|
|
});
|
|
}
|
|
|
|
// for external usage, do not expose path/html/internal details
|
|
async listSimpleWebviews(): Promise<WebviewSimpleInfo[]> {
|
|
return Array.from(this.#webviews.entries()).map(entry => {
|
|
const id = entry[0];
|
|
const webviewPanelImpl = entry[1];
|
|
const viewType: string = webviewPanelImpl.viewType;
|
|
return {
|
|
id,
|
|
viewType,
|
|
title: webviewPanelImpl.title,
|
|
};
|
|
});
|
|
}
|
|
}
|