// HTTP as FILIUS writes it (http.ts), and the web server and browser // (apps/web.ts) where webJava.test.ts's stories do not go. import { describe, expect, it } from 'vitest'; import { decodeUriPath, deviceConfiguration, encodeUriPath, formatHttpRequest, formatHttpResponse, imageSources, javaBase64, linkTarget, pageReference, javaBase64Decode, parseHttpRequest, parseHttpResponse, VirtualFileSystem, webBrowserAddress, type NetworkRuntimeEngine, type TopologySnapshotInput, } from '../src/index.js'; import { link, pc, startedEngine, uuid } from './fixtures.js'; describe('HTTP as FILIUS writes it', () => { it('writes a request with the path encoded and without its query, and a POST with its data', () => { expect(formatHttpRequest({ method: 'GET', path: '/mein bild.html?x=1', host: 'www.filius.de' })).toBe('GET /mein%20bild.html HTTP/1.1\nHost: www.filius.de\n'); expect(formatHttpRequest({ method: 'POST', path: '', host: undefined, data: 'a=b' })).toBe('POST / HTTP/1.1\n\na=b'); }); it('reads a request: method in any case, Host header, POST data after the empty line', () => { expect(parseHttpRequest('post /form%C3%BC HTTP/1.0\nhost: a.de\n\nx=1\ny=2')).toEqual({ type: 'request', request: { method: 'post', path: '/formü', protocolVersion: 'HTTP/1.0', host: 'a.de', data: '\nx=1\ny=2' }, }); }); it('answers what it cannot read with 400 or 501, and takes a request without a version as HTTP/1.1 (Java: the thread dies)', () => { expect(parseHttpRequest('')).toEqual({ type: 'invalid', statusCode: 400 }); expect(parseHttpRequest('GET')).toEqual({ type: 'invalid', statusCode: 400 }); expect(parseHttpRequest('HALLO Welt')).toEqual({ type: 'invalid', statusCode: 501 }); expect(parseHttpRequest('GET /%zz HTTP/1.1')).toEqual({ type: 'invalid', statusCode: 400 }); expect(parseHttpRequest('GET /')).toMatchObject({ type: 'request', request: { path: '/', protocolVersion: 'HTTP/1.1' } }); }); it('writes and reads a response', () => { const text = formatHttpResponse({ protocolVersion: 'HTTP/1.1', statusCode: 200, contentType: 'text/html', body: '
\n
' }); expect(text).toBe('HTTP/1.1 200 OK\nContent-type: text/html\n\n\n
'); expect(parseHttpResponse(text)).toEqual({ protocolVersion: 'HTTP/1.1', statusCode: 200, contentType: 'text/html', body: '\n
' }); expect(parseHttpResponse('HTTP/1.1 404 Not Found\n')).toEqual({ protocolVersion: 'HTTP/1.1', statusCode: 404, contentType: undefined, body: undefined }); expect(parseHttpResponse('HTTP/1.1 200 OK\ncontent-TYPE: image/png\n\nx')).toMatchObject({ contentType: 'image/png', body: 'x' }); expect(parseHttpResponse('Hallo Welt')).toBeUndefined(); }); it('encodes a path as Spring UriUtils.encodePath and decodes it back', () => { expect(encodeUriPath("/a b/ü?#%[]-._~!$&'()*+,;=:@")).toBe("/a%20b/%C3%BC%3F%23%25%5B%5D-._~!$&'()*+,;=:@"); expect(decodeUriPath('/a%20b/%C3%BC+')).toBe('/a b/ü+'); expect(decodeUriPath('/50%')).toBeUndefined(); }); it('reads and writes Base64 as FILIUS does, line breaks and all', () => { const bytes = new Uint8Array(100).map((_, index) => index * 7); expect(javaBase64Decode(javaBase64(bytes))).toEqual(bytes); expect(javaBase64Decode('QUJD\nRA==')).toEqual(new TextEncoder().encode('ABCD')); expect(javaBase64Decode('QUJ')).toBeUndefined(); expect(javaBase64Decode('QU=D')).toBeUndefined(); }); }); describe("the browser's address line (erzeugeURL)", () => { it('takes a host with or without http://, the path without a final slash', () => { expect(webBrowserAddress('www.filius.de')).toEqual({ host: 'www.filius.de', path: '/' }); expect(webBrowserAddress('HTTP://192.168.0.30/a/b/')).toEqual({ host: '192.168.0.30', path: '/a/b' }); expect(webBrowserAddress('http:///text.txt')).toEqual({ host: '', path: '/text.txt' }); expect(webBrowserAddress('/text.txt')).toEqual({ host: '', path: '/text.txt' }); }); it('is no address for what Java does not take as a host', () => { expect(webBrowserAddress('www.filius.de:8080/')).toBeUndefined(); expect(webBrowserAddress('1fehler.de')).toBeUndefined(); expect(webBrowserAddress('http:')).toBeUndefined(); }); }); describe("a page's references", () => { it('count from the page\'s folder, written as Java writes them for a page in the root (Java counts from the root)', () => { expect(pageReference('/', 'bild.png')).toBe('bild.png'); expect(pageReference('/seite.html?x=1/2', 'unter/bild.png')).toBe('unter/bild.png'); expect(pageReference('/ordner/seite.html', 'bild.png')).toBe('ordner/bild.png'); expect(pageReference('/vhost1/', '../bild.png')).toBe('vhost1/../bild.png'); expect(pageReference('ohne/schraegstrich.html', 'a.png')).toBe('ohne/a.png'); expect(pageReference('/ordner/seite.html', '/bild.png')).toBe('/bild.png'); expect(pageReference('/ordner/seite.html', 'http://h/x.png')).toBe('http://h/x.png'); }); it('lead a link to the page\'s host, an http address to its own, and nowhere for a mark or another scheme', () => { const page = { host: 'www.filius.de', path: '/ordner/seite.html' }; expect(linkTarget(page, 'vier.html')).toEqual({ host: 'www.filius.de', path: '/ordner/vier.html' }); expect(linkTarget(page, ' /text.txt ')).toEqual({ host: 'www.filius.de', path: '/text.txt' }); expect(linkTarget(page, 'ziel.html?a=1#oben')).toEqual({ host: 'www.filius.de', path: '/ordner/ziel.html?a=1' }); expect(linkTarget({ host: 'h', path: '/' }, 'a.html')).toEqual({ host: 'h', path: '/a.html' }); expect(linkTarget(page, 'HTTP://192.168.0.30/x/y#z')).toEqual({ host: '192.168.0.30', path: '/x/y' }); expect(linkTarget(page, 'http://kein host/')).toBeUndefined(); for (const href of ['', ' ', '#oben', 'mailto:anna@filius.de', 'javascript:alert(1)', 'ftp://h/']) expect(linkTarget(page, href), href).toBeUndefined(); }); }); describe('the images of a page', () => { it('finds each img tag, in any case, its src in any quotes', () => { expect(imageSources('


', '/webserver/seite.html': 'Seite' });
fileSystem.writeImageFile('/webserver/a.png', new Uint8Array([9, 8, 7]), 'image/png');
const engine = startedEngine(network(fileSystem));
const browser = engine.webBrowser(CLIENT)!;
expect(browser.open('10.0.0.1')).toBe(true);
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
expect(browser.page).toMatchObject({ statusCode: 200, body: '
' });
expect([...browser.page!.images]).toEqual([['a.png', new Uint8Array([9, 8, 7])]]);
expect(browser.log.all.filter((line) => line.kind === 'image').map((line) => line.kind === 'image' && line.statusCode)).toEqual([200, 404]);
browser.load('', '/seite.html');
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
expect(browser.host).toBe('10.0.0.1');
expect(browser.page).toMatchObject({ statusCode: 200, body: 'Seite' });
expect(browser.page!.images.size).toBe(0);
});
it("asks for a page's images from the page's folder and keeps them by their src, with the page's host and path", () => {
const fileSystem = files({ '/webserver/ordner/seite.html': '
' });
fileSystem.writeImageFile('/webserver/ordner/a.png', new Uint8Array([1]), 'image/png');
fileSystem.writeImageFile('/webserver/b.png', new Uint8Array([2]), 'image/png');
const engine = startedEngine(network(fileSystem));
const browser = engine.webBrowser(CLIENT)!;
browser.open('10.0.0.1/ordner/seite.html?x=1');
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
expect(browser.page).toMatchObject({ host: '10.0.0.1', path: '/ordner/seite.html?x=1' });
expect([...browser.page!.images]).toEqual([['a.png', new Uint8Array([1])], ['/b.png', new Uint8Array([2])]]);
const lines = browser.log.all.map(({ timeMilliseconds: _, ...line }) => line);
expect(lines).toEqual([
{ kind: 'page', host: '10.0.0.1', path: '/ordner/seite.html?x=1', statusCode: 200, contentType: 'text/html', body: '
' },
{ kind: 'image', source: 'a.png', statusCode: 200, contentType: 'image/png', data: new Uint8Array([1]) },
{ kind: 'image', source: '/b.png', statusCode: 200, contentType: 'image/png', data: new Uint8Array([2]) },
]);
});
it('fetches images only for an HTML page', () => {
const engine = startedEngine(network(files({ '/webserver/text.txt': '
' })));
const browser = engine.webBrowser(CLIENT)!;
browser.open('10.0.0.1/text.txt');
engine.handle({ type: 'advance', toMilliseconds: 10_000 });
expect(browser.log.all.map((line) => line.kind)).toEqual(['page']);
});
it('does nothing for what is no address', () => {
const engine = startedEngine(network(files({})));
expect(engine.webBrowser(CLIENT)!.open('kein host:80')).toBe(false);
engine.handle({ type: 'advance', toMilliseconds: 5_000 });
expect(engine.webBrowser(CLIENT)!.log.all).toEqual([]);
expect(engine.capture.frames(uuid(0x3012))).toEqual([]);
});
it('shows "server not reachable" when the server closes without an answer (Java shows nothing)', () => {
const engine = startedEngine(network(files({}), { webServersByNodeId: new Map() }));
const listener = engine.openTcpServerSocket(SERVER, 80)!;
const browser = engine.webBrowser(CLIENT)!;
browser.open('10.0.0.1');
for (let time = 100; time < 20_000; time += 100) {
engine.handle({ type: 'advance', toMilliseconds: time });
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listener)?.[0];
if (accepted !== undefined && engine.state.socketsById.get(accepted)?.tcpState === 'ESTABLISHED') engine.closeTcpSocket(accepted);
}
expect(browser.log.all.map((line) => line.kind)).toEqual(['unreachable']);
});
});