feat(editor): F4.6d the web browser and web server on a computer's desktop
The oracle BrowserWindowRun uses Java's browser and web server windows as a person does; the room keeps a browser window per computer and sends a program window's view, the start names the person's language. The editor shows the page in a sandboxed frame, links and forms go to the room, the web server has its button, virtual hosts and lines. Better than Java: relative images and links count from the page's folder, a mark stays in the page, the error page from Java's template, the virtual hosts' file with complete rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,13 @@
|
||||
// cannot read gets 400 or 501, one without a version is HTTP/1.1 (Java's
|
||||
// thread dies of it and closes the connection); the browser shows "server
|
||||
// not reachable" when its request cannot go out or the answer cannot be
|
||||
// read (Java shows nothing).
|
||||
// read (Java shows nothing). A page's relative images and links count from
|
||||
// its folder, as in every browser (Java's window counts them from the
|
||||
// server's root, so that a page in a folder misses its images and links;
|
||||
// for a page in the root the requests are Java's). The table of virtual
|
||||
// hosts is written with its complete rows only (Java writes every row, and
|
||||
// reading the file again a row with an empty cell takes the next row's
|
||||
// cell).
|
||||
//
|
||||
// A server may make pages itself (Java's plug-ins, WebServerPlugIn): a
|
||||
// router's or home router's firewall log and routing table (routerPages.ts).
|
||||
@@ -74,10 +80,13 @@ function contentTypeOf(name: string): string {
|
||||
|
||||
const IMAGE_TYPES = new Set(['image/bmp', 'image/gif', 'image/jpg', 'image/png']);
|
||||
|
||||
/** WebServer's table of virtual hosts: five rows of host name and folder, "" where empty. */
|
||||
export type VirtualHostTable = readonly (readonly [string, string])[];
|
||||
|
||||
export class WebServer extends TcpServerProgram {
|
||||
private readonly documentRoot: string;
|
||||
private readonly virtualHosts: readonly (readonly [string, string])[];
|
||||
private readonly useVirtualHosts: boolean;
|
||||
private virtualHosts: [string, string][];
|
||||
private useVirtualHostsValue: boolean;
|
||||
|
||||
constructor(
|
||||
engine: NetworkRuntimeEngine,
|
||||
@@ -96,13 +105,49 @@ export class WebServer extends TcpServerProgram {
|
||||
for (let index = 0; index < lines.length && rows.length < VIRTUAL_HOST_ROWS; index += 2) {
|
||||
rows.push([lines[index]!, lines[index + 1] ?? '']);
|
||||
}
|
||||
while (rows.length < VIRTUAL_HOST_ROWS) rows.push(['', '']);
|
||||
this.virtualHosts = rows;
|
||||
this.useVirtualHosts = configuration.virtualHosts || vhosts !== undefined;
|
||||
this.useVirtualHostsValue = configuration.virtualHosts || vhosts !== undefined;
|
||||
}
|
||||
|
||||
/** Whether the server looks up the request's host in its table (the window's check box). */
|
||||
get useVirtualHosts(): boolean {
|
||||
return this.useVirtualHostsValue;
|
||||
}
|
||||
|
||||
/** WebServer.setUseVHost: from the next request on. */
|
||||
setUseVirtualHosts(use: boolean): void {
|
||||
this.useVirtualHostsValue = use;
|
||||
}
|
||||
|
||||
get virtualHostTable(): VirtualHostTable {
|
||||
return this.virtualHosts;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebServer.changeVHostTable: a cell of the table (column 0 the host name,
|
||||
* 1 the folder), saved to /www.conf/vhosts — with the complete rows only,
|
||||
* so that Java reads the file as the table is.
|
||||
*/
|
||||
setVirtualHost(row: number, column: 0 | 1, value: string): void {
|
||||
const cells = this.virtualHosts[row];
|
||||
if (cells === undefined) return;
|
||||
cells[column] = value;
|
||||
const text = this.virtualHosts
|
||||
.filter(([host, folder]) => host !== '' && folder !== '')
|
||||
.map(([host, folder]) => `${host}\n${folder}\n`)
|
||||
.join('');
|
||||
try {
|
||||
if (this.fileSystem !== undefined && !this.fileSystem.contains('/www.conf')) this.fileSystem.createDirectory('/www.conf');
|
||||
this.fileSystem?.writeTextFile('/www.conf/vhosts', text);
|
||||
} catch {
|
||||
// A file where the folder should be: the table holds, unsaved, as Java's does when it cannot save.
|
||||
}
|
||||
}
|
||||
|
||||
/** WebServer.vhostPrefix: the folder of the host name, ignoring case; "" when none. */
|
||||
private virtualHostFolder(host: string | undefined): string {
|
||||
if (!this.useVirtualHosts || host === undefined) return '';
|
||||
if (!this.useVirtualHostsValue || host === undefined) return '';
|
||||
return this.virtualHosts.find(([name]) => name.toLowerCase() === host.toLowerCase())?.[1] ?? '';
|
||||
}
|
||||
|
||||
@@ -189,6 +234,35 @@ function javaSplit(text: string, separator: string): string[] {
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* A page's reference (an image's src) as the browser requests it: one with
|
||||
* a leading slash or a scheme as it is, a relative one from the page's
|
||||
* folder, without a leading slash as Java writes it (for a page in the
|
||||
* root, Java's request).
|
||||
*/
|
||||
export function pageReference(pagePath: string, reference: string): string {
|
||||
if (reference.startsWith('/') || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(reference)) return reference;
|
||||
const path = pagePath.split('?')[0]!;
|
||||
return path.slice(path.startsWith('/') ? 1 : 0, path.lastIndexOf('/') + 1) + reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a link of a page leads, as the browser window loads it: an http
|
||||
* address to its host, a path to the page's host (relative from the page's
|
||||
* folder, with a leading slash as Java's window writes it); undefined for a
|
||||
* link the browser does not follow (another scheme, a mark in the page).
|
||||
*/
|
||||
export function linkTarget(page: { readonly host: string; readonly path: string }, href: string): { readonly host: string; readonly path: string } | undefined {
|
||||
const reference = href.trim();
|
||||
if (reference === '' || reference.startsWith('#')) return undefined;
|
||||
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(reference)) {
|
||||
if (!/^http:/i.test(reference)) return undefined;
|
||||
return webBrowserAddress(reference.split('#')[0]!);
|
||||
}
|
||||
const target = pageReference(page.path, reference.split('#')[0]!);
|
||||
return { host: page.host, path: target.startsWith('/') ? target : `/${target}` };
|
||||
}
|
||||
|
||||
/** EingabenUeberpruefung.musterDomain and musterIpAdresse. */
|
||||
const DOMAIN_PATTERN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9]){0,1}(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]){0,1})*){0,1}\.{0,1}$/;
|
||||
export const IP_ADDRESS_PATTERN =
|
||||
@@ -218,15 +292,24 @@ export function imageSources(html: string): string[] {
|
||||
}
|
||||
|
||||
export type WebBrowserLogEntry =
|
||||
/** A page came: shown as it is with status 200, else as the browser's error page for the status. */
|
||||
| { readonly kind: 'page'; readonly statusCode: number; readonly contentType: string | undefined; readonly body: string | undefined }
|
||||
/** A page came from the host at the path: shown as it is with status 200, else as the browser's error page for the status. */
|
||||
| {
|
||||
readonly kind: 'page';
|
||||
readonly host: string;
|
||||
readonly path: string;
|
||||
readonly statusCode: number;
|
||||
readonly contentType: string | undefined;
|
||||
readonly body: string | undefined;
|
||||
}
|
||||
/** sw_webbrowser_msg1: the server could not be reached (no address, no connection, no answer). */
|
||||
| { readonly kind: 'unreachable' }
|
||||
/** An image of the page came (or not: its status); Java tells its window, which loads it anew. */
|
||||
| { readonly kind: 'image'; readonly source: string; readonly statusCode: number; readonly contentType: string | undefined };
|
||||
/** An image of the page (its src as written) came, with its data — or not: its status; Java tells its window, which loads it anew. */
|
||||
| { readonly kind: 'image'; readonly source: string; readonly statusCode: number; readonly contentType: string | undefined; readonly data?: Uint8Array };
|
||||
|
||||
/** The page the browser shows: as it came, with the images that came for it. */
|
||||
/** The page the browser shows: as it came, with the images that came for it (by their src as written). */
|
||||
export interface WebBrowserPage {
|
||||
readonly host: string;
|
||||
readonly path: string;
|
||||
readonly statusCode: number;
|
||||
readonly contentType: string | undefined;
|
||||
readonly body: string | undefined;
|
||||
@@ -238,7 +321,7 @@ export class WebBrowser {
|
||||
private hostValue: string | undefined;
|
||||
private socketId: Id | undefined;
|
||||
private images: string[] = [];
|
||||
private shown: { statusCode: number; contentType: string | undefined; body: string | undefined; images: Map<string, Uint8Array> } | undefined;
|
||||
private shown: (Omit<WebBrowserPage, 'images'> & { readonly images: Map<string, Uint8Array> }) | undefined;
|
||||
private readonly commands: CommandThread;
|
||||
|
||||
constructor(
|
||||
@@ -326,22 +409,22 @@ export class WebBrowser {
|
||||
if (response.statusCode === 200 && response.contentType?.toLowerCase() === 'text/html' && response.body !== undefined) {
|
||||
this.images = imageSources(response.body);
|
||||
}
|
||||
this.shown = { statusCode: response.statusCode, contentType: response.contentType, body: response.body, images: new Map() };
|
||||
this.log.write({ kind: 'page', statusCode: response.statusCode, contentType: response.contentType, body: response.body });
|
||||
const page = { host: this.hostValue ?? '', path, statusCode: response.statusCode, contentType: response.contentType, body: response.body };
|
||||
this.shown = { ...page, images: new Map() };
|
||||
this.log.write({ kind: 'page', ...page });
|
||||
}
|
||||
|
||||
/** WebBrowser.retrieveImages: each over the same connection, as long as it lasts. */
|
||||
/** WebBrowser.retrieveImages: each over the same connection, as long as it lasts, from the page's folder. */
|
||||
private *retrieveImages(): Steps {
|
||||
const pagePath = this.shown?.path ?? '/';
|
||||
for (const source of this.images) {
|
||||
if (!this.isConnected) return;
|
||||
const response = yield* this.exchange(source, undefined);
|
||||
const response = yield* this.exchange(pageReference(pagePath, source), undefined);
|
||||
if (response === undefined) continue;
|
||||
const isImage = response.contentType !== undefined && IMAGE_TYPES.has(response.contentType.toLowerCase());
|
||||
if (response.statusCode === 200 && isImage) {
|
||||
const data = javaBase64Decode(response.body ?? '');
|
||||
if (data !== undefined) this.shown?.images.set(source, data);
|
||||
}
|
||||
this.log.write({ kind: 'image', source, statusCode: response.statusCode, contentType: response.contentType });
|
||||
const data = response.statusCode === 200 && isImage ? javaBase64Decode(response.body ?? '') : undefined;
|
||||
if (data !== undefined) this.shown?.images.set(source, data);
|
||||
this.log.write({ kind: 'image', source, statusCode: response.statusCode, contentType: response.contentType, ...(data === undefined ? {} : { data }) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user