diff --git a/src/parts/CrossOriginEmbedderPolicy/CrossOriginEmbedderPolicy.ts b/src/parts/CrossOriginEmbedderPolicy/CrossOriginEmbedderPolicy.ts new file mode 100644 index 00000000..88ee8bb3 --- /dev/null +++ b/src/parts/CrossOriginEmbedderPolicy/CrossOriginEmbedderPolicy.ts @@ -0,0 +1 @@ +export const value = 'require-corp' diff --git a/src/parts/CrossOriginResourcePolicy/CrossOriginResourcePolicy.ts b/src/parts/CrossOriginResourcePolicy/CrossOriginResourcePolicy.ts new file mode 100644 index 00000000..f1af447b --- /dev/null +++ b/src/parts/CrossOriginResourcePolicy/CrossOriginResourcePolicy.ts @@ -0,0 +1 @@ +export const value = 'cross-origin' diff --git a/src/parts/FileSystem/FileSystem.ts b/src/parts/FileSystem/FileSystem.ts new file mode 100644 index 00000000..cdba47d5 --- /dev/null +++ b/src/parts/FileSystem/FileSystem.ts @@ -0,0 +1,6 @@ +import * as nodeFs from 'node:fs/promises' + +export const readFile = async (url: string) => { + const buffer = await nodeFs.readFile(url) + return buffer +} diff --git a/src/parts/GetElectronFileResponseAbsolutePath/GetElectronFileResponseAbsolutePath.ts b/src/parts/GetElectronFileResponseAbsolutePath/GetElectronFileResponseAbsolutePath.ts new file mode 100644 index 00000000..1aaaa709 --- /dev/null +++ b/src/parts/GetElectronFileResponseAbsolutePath/GetElectronFileResponseAbsolutePath.ts @@ -0,0 +1,18 @@ +const getPathName = (url: string) => { + try { + const p = new URL(url).pathname + return p + } catch { + return '' + } +} + +export const getElectronFileResponseAbsolutePath = (url: string) => { + // TODO support windows paths + // TODO disallow dot dot in paths + const pathName = getPathName(url) + if (pathName.endsWith('/')) { + return pathName + 'index.html' + } + return pathName +} diff --git a/src/parts/GetHeaders/GetHeaders.ts b/src/parts/GetHeaders/GetHeaders.ts new file mode 100644 index 00000000..2e85f913 --- /dev/null +++ b/src/parts/GetHeaders/GetHeaders.ts @@ -0,0 +1,18 @@ +import { extname } from 'node:path' +import * as CrossOriginEmbedderPolicy from '../CrossOriginEmbedderPolicy/CrossOriginEmbedderPolicy.ts' +import * as CrossOriginResourcePolicy from '../CrossOriginResourcePolicy/CrossOriginResourcePolicy.ts' +import * as GetMimeType from '../GetMimeType/GetMimeType.ts' +import * as HttpHeader from '../HttpHeader/HttpHeader.ts' + +export const getHeaders = (absolutePath: string) => { + const extension = extname(absolutePath) + const mime = GetMimeType.getMimeType(extension) + const headers = { + [HttpHeader.ContentType]: mime, + [HttpHeader.CrossOriginResourcePolicy]: CrossOriginResourcePolicy.value, + [HttpHeader.CrossOriginEmbedderPolicy]: CrossOriginEmbedderPolicy.value, + } + return { + ...headers, + } +} diff --git a/src/parts/GetMimeType/GetMimeType.ts b/src/parts/GetMimeType/GetMimeType.ts new file mode 100644 index 00000000..caca6126 --- /dev/null +++ b/src/parts/GetMimeType/GetMimeType.ts @@ -0,0 +1,28 @@ +import * as MimeType from '../MimeType/MimeType.ts' + +export const getMimeType = (fileExtension: string) => { + switch (fileExtension) { + case '.html': + return MimeType.TextHtml + case '.css': + return MimeType.TextCss + case '.ttf': + return MimeType.FontTtf + case '.js': + case '.mjs': + case '.ts': + return MimeType.TextJavaScript + case '.svg': + return MimeType.ImageSvgXml + case '.png': + return MimeType.ImagePng + case '.json': + case '.map': + return MimeType.ApplicationJson + case '.mp3': + return MimeType.AudioMpeg + default: + console.warn(`unsupported file extension: ${fileExtension}`) + return '' + } +} diff --git a/src/parts/HttpHeader/HttpHeader.ts b/src/parts/HttpHeader/HttpHeader.ts new file mode 100644 index 00000000..5765d3ab --- /dev/null +++ b/src/parts/HttpHeader/HttpHeader.ts @@ -0,0 +1,8 @@ +export const CacheControl = 'Cache-Control' +export const ContentType = 'Content-Type' +export const ContentSecurityPolicy = 'Content-Security-Policy' +export const CrossOriginEmbedderPolicy = 'Cross-Origin-Embedder-Policy' +export const CrossOriginOpenerPolicy = 'Cross-Origin-Opener-Policy' +export const CrossOriginResourcePolicy = 'Cross-Origin-Resource-Policy' +export const Etag = 'Etag' +export const IfNotMatch = 'if-none-match' diff --git a/src/parts/HttpStatusCode/HttpStatusCode.ts b/src/parts/HttpStatusCode/HttpStatusCode.ts index 244a717f..101df56a 100644 --- a/src/parts/HttpStatusCode/HttpStatusCode.ts +++ b/src/parts/HttpStatusCode/HttpStatusCode.ts @@ -1,2 +1,3 @@ export const MethodNotAllowed = 405 +export const NotFound = 404 export const Ok = 200 diff --git a/src/parts/MimeType/MimeType.ts b/src/parts/MimeType/MimeType.ts new file mode 100644 index 00000000..2b5ae603 --- /dev/null +++ b/src/parts/MimeType/MimeType.ts @@ -0,0 +1,37 @@ +export const ApplicationFontWoff = 'application/font-woff' +export const ApplicationJson = 'application/json' +export const AudioMidi = 'audio/midi' +export const AudioMpeg = 'audio/mpeg' +export const AudioOgg = 'audio/ogg' +export const AudioOpus = 'audio/opus' +export const AudioXaac = 'audio/x-aac' +export const AudioXMsWma = 'audio/x-ms-wma' +export const AudioXWav = 'audio/x-wav' +export const FontTtf = 'font/ttf' +export const ImageAvif = 'image/avif' +export const ImageBmp = 'image/bmp' +export const ImageGif = 'image/gif' +export const ImageJpg = 'image/jpg' +export const ImagePng = 'image/png' +export const ImageSvgXml = 'image/svg+xml' +export const ImageTiff = 'image/tiff' +export const ImageVndAdobePhotoShop = 'image/vnd.adobe.photoshop' +export const ImageWebp = 'image/webp' +export const ImageXIcon = 'image/x-icon' +export const ImageXTga = 'image/x-tga' +export const TextCalendar = 'text/calendar' +export const TextCss = 'text/css' +export const TextCvs = 'text/csv' +export const TextHtml = 'text/html' +export const TextJavaScript = 'text/javascript' +export const TextPlain = 'text/plain' +export const TextXml = 'text/xml' +export const VideoMp4 = 'video/mp4' +export const VideoMpeg = 'video/mpeg' +export const VideoQuickTime = 'video/quicktime' +export const VideoWebm = 'video/webm' +export const VideoXFlv = 'video/x-flv' +export const VideoXMatroska = 'video/x-matroska' +export const VideoXMsVideo = 'video/x-msvideo' +export const VideoXMsWmv = 'video/x-ms-wmv' +export const VideoXSgiMovie = 'video/x-sgi-movie' diff --git a/src/parts/WebViewProtocol/WebViewProtocol.ts b/src/parts/WebViewProtocol/WebViewProtocol.ts index 4cf84be5..690e99a5 100644 --- a/src/parts/WebViewProtocol/WebViewProtocol.ts +++ b/src/parts/WebViewProtocol/WebViewProtocol.ts @@ -1,28 +1,42 @@ +import * as FileSystem from '../FileSystem/FileSystem.ts' +import * as GetElectronFileResponseAbsolutePath from '../GetElectronFileResponseAbsolutePath/GetElectronFileResponseAbsolutePath.ts' +import * as GetHeaders from '../GetHeaders/GetHeaders.ts' import * as HttpMethod from '../HttpMethod/HttpMethod.ts' import * as HttpStatusCode from '../HttpStatusCode/HttpStatusCode.ts' -export const getResponse = (method: string, url: string) => { +const defaultHeaders = { + 'Cross-Origin-Resource-Policy': 'cross-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', +} + +export const getResponse = async (method: string, url: string) => { // TODO allow head requests if (method !== HttpMethod.Get) { return { body: 'Method not allowed', init: { status: HttpStatusCode.MethodNotAllowed, - headers: { - 'Cross-Origin-Resource-Policy': 'cross-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, + headers: defaultHeaders, + }, + } + } + const absolutePath = GetElectronFileResponseAbsolutePath.getElectronFileResponseAbsolutePath(url) + if (!absolutePath) { + return { + body: 'not found', + init: { + status: HttpStatusCode.NotFound, + headers: defaultHeaders, }, } } + const content = await FileSystem.readFile(absolutePath) + const headers = GetHeaders.getHeaders(absolutePath) return { - body: 'test 123', + body: content, init: { status: HttpStatusCode.Ok, - headers: { - 'Cross-Origin-Resource-Policy': 'cross-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, + headers, }, } } diff --git a/test/WebViewProtocol.test.ts b/test/WebViewProtocol.test.ts index d9047049..38dd03fd 100644 --- a/test/WebViewProtocol.test.ts +++ b/test/WebViewProtocol.test.ts @@ -1,7 +1,19 @@ -import { expect, test } from '@jest/globals' +import { beforeEach, expect, jest, test } from '@jest/globals' import * as HttpMethod from '../src/parts/HttpMethod/HttpMethod.ts' import * as HttpStatusCode from '../src/parts/HttpStatusCode/HttpStatusCode.ts' -import * as WebViewProtocol from '../src/parts/WebViewProtocol/WebViewProtocol.ts' + +beforeEach(() => { + jest.resetAllMocks() +}) + +jest.unstable_mockModule('../src/parts/FileSystem/FileSystem.ts', () => { + return { + readFile: jest.fn(), + } +}) + +const WebViewProtocol = await import('../src/parts/WebViewProtocol/WebViewProtocol.ts') +const FileSystem = await import('../src/parts/FileSystem/FileSystem.ts') test('method not allowed - post', async () => { const method = HttpMethod.Post @@ -20,12 +32,14 @@ test('method not allowed - post', async () => { test('get', async () => { const method = HttpMethod.Get - const url = '/test/media' + const url = 'lvce-webview://-/test/media/' + jest.spyOn(FileSystem, 'readFile').mockResolvedValue(Buffer.from('a')) expect(await WebViewProtocol.getResponse(method, url)).toEqual({ - body: 'test 123', + body: Buffer.from('a'), init: { status: HttpStatusCode.Ok, headers: { + 'Content-Type': 'text/html', 'Cross-Origin-Resource-Policy': 'cross-origin', 'Cross-Origin-Embedder-Policy': 'require-corp', },