-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
223 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
electron/main/electron-next/__tests__/is-safe-path.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { isSafePath } from '../is-safe-path.js'; | ||
|
||
describe('is-safe-path', () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers({ shouldAdvanceTime: true }); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
vi.clearAllTimers(); | ||
vi.useRealTimers(); | ||
}); | ||
|
||
describe('#isSafePath', () => { | ||
it('returns true when file path is within the directory', async () => { | ||
expect( | ||
isSafePath({ | ||
dirPath: '/a/b/', | ||
filePath: '/a/b/c.html', | ||
}) | ||
).toBe(true); | ||
|
||
expect( | ||
isSafePath({ | ||
dirPath: '/a/b/', | ||
filePath: '/a/b/c/d.html', | ||
}) | ||
).toBe(true); | ||
}); | ||
|
||
it('returns false when file path is outside the directory', async () => { | ||
expect( | ||
isSafePath({ | ||
dirPath: '/a/b/', | ||
filePath: '', | ||
}) | ||
).toBe(false); | ||
|
||
expect( | ||
isSafePath({ | ||
dirPath: '/a/b/', | ||
filePath: '/a/b/', | ||
}) | ||
).toBe(false); | ||
|
||
expect( | ||
isSafePath({ | ||
dirPath: '/a/b/', | ||
filePath: './a/b/c.html', | ||
}) | ||
).toBe(false); | ||
|
||
expect( | ||
isSafePath({ | ||
dirPath: '/a/b/', | ||
filePath: '../a/b/c.html', | ||
}) | ||
).toBe(false); | ||
}); | ||
}); | ||
}); |
25 changes: 25 additions & 0 deletions
25
electron/main/electron-next/__tests__/path-to-file-url.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { pathToFileURL } from '../path-to-file-url.js'; | ||
|
||
describe('path-to-file-url', () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers({ shouldAdvanceTime: true }); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
vi.clearAllTimers(); | ||
vi.useRealTimers(); | ||
}); | ||
|
||
describe('#pathToFileURL', () => { | ||
it('...', async () => { | ||
expect( | ||
pathToFileURL({ | ||
dirPath: '/a/b/', | ||
filePath: '/a/b/c.html', | ||
}) | ||
).toBe(true); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "../../tsconfig.test.json" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import path from 'node:path'; | ||
|
||
/** | ||
* Determine if a file path is safe to serve by | ||
* ensuring that it is within the directory path. | ||
* Protects against directory traversal attacks. | ||
*/ | ||
export const isSafePath = (options: { | ||
/** | ||
* Known safe directory to host files from. | ||
* Example: '/path/to/directory' | ||
*/ | ||
dirPath: string; | ||
/** | ||
* A file path to verify is within the directory. | ||
* Example: '/path/to/directory/file.txt' | ||
*/ | ||
filePath: string; | ||
}): boolean => { | ||
const { dirPath, filePath } = options; | ||
|
||
const relativePath = path.relative(dirPath, filePath); | ||
|
||
const isSafe = | ||
relativePath.length > 0 && | ||
!relativePath.startsWith('..') && | ||
!path.isAbsolute(relativePath); | ||
|
||
return isSafe; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import path from 'node:path'; | ||
import url from 'node:url'; | ||
|
||
/** | ||
* Converts a file path to an absolute file URL. | ||
* Example: '/path/to/file.txt' -> 'file:///path/to/file.txt' | ||
*/ | ||
export const pathToFileURL = (options: { | ||
dirPath: string; | ||
filePath: string; | ||
}): string => { | ||
const { dirPath, filePath } = options; | ||
return url.pathToFileURL(path.join(dirPath, filePath)).toString(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/** | ||
* This module enables serving static files generated by Nextjs in production. | ||
* It's inspired by the project `electron-serve` developed by Sindre Sorhus. | ||
* https://github.com/sindresorhus/electron-serve | ||
* | ||
* It registers a custom protocol to serve files from a directory. | ||
* https://www.electronjs.org/docs/latest/api/protocol | ||
* | ||
* After porting my project to ESM, the `electron-serve` module was no longer | ||
* resolving the file paths correctly. | ||
* https://github.com/sindresorhus/electron-serve/issues/29 | ||
* | ||
* As a workaround, I re-implemented the logic here. | ||
*/ | ||
|
||
import { app, net, protocol } from 'electron'; | ||
import path from 'node:path'; | ||
import { isSafePath } from './is-safe-path.js'; | ||
import { logger } from './logger.js'; | ||
import { pathToFileURL } from './path-to-file-url.js'; | ||
|
||
export const prodServe = (options: { | ||
/** | ||
* The protocol to serve the directory on. | ||
* All URL requests that use this protocol will be served from the directory. | ||
*/ | ||
scheme: string; | ||
/** | ||
* The directory to serve, relative to the app root directory. | ||
*/ | ||
dirPath: string; | ||
}): void => { | ||
const { scheme, dirPath } = options; | ||
|
||
logger.info('registering protocol scheme', { | ||
scheme, | ||
dirPath, | ||
}); | ||
|
||
const error404Page = pathToFileURL({ dirPath, filePath: '404.html' }); | ||
|
||
const requestHandler = async (httpReq: Request): Promise<Response> => { | ||
const requestURL = new URL(httpReq.url); | ||
|
||
let pageToServe = error404Page; | ||
|
||
let pathname = requestURL.pathname; | ||
if (pathname === '/') { | ||
pathname = 'index.html'; | ||
} | ||
|
||
// Prevent loading files outside of the renderer directory. | ||
const pathToServe = path.join(dirPath, pathname); | ||
const isSafe = isSafePath({ dirPath, filePath: pathToServe }); | ||
|
||
if (isSafe) { | ||
pageToServe = pathToFileURL({ dirPath, filePath: pathToServe }); | ||
} else { | ||
pageToServe = error404Page; | ||
} | ||
|
||
return net.fetch(pageToServe); | ||
}; | ||
|
||
protocol.registerSchemesAsPrivileged([ | ||
{ | ||
scheme, | ||
privileges: { | ||
standard: true, | ||
secure: true, | ||
supportFetchAPI: true, | ||
allowServiceWorkers: true, | ||
}, | ||
}, | ||
]); | ||
|
||
app.on('ready', () => { | ||
protocol.handle(scheme, requestHandler); | ||
}); | ||
}; |