一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
184 行
6.6 KiB
TypeScript
184 行
6.6 KiB
TypeScript
/** @jest-environment node */
|
|
|
|
type ProtocolHandler = (request: Request)=> Promise<Response>;
|
|
const customProtocols: Map<string, ProtocolHandler> = new Map();
|
|
|
|
const handleProtocolMock = jest.fn((scheme: string, handler: ProtocolHandler) => {
|
|
customProtocols.set(scheme, handler);
|
|
});
|
|
const fetchMock = jest.fn(async url => new Response(`Mock response to ${url}`));
|
|
|
|
jest.doMock('electron', () => {
|
|
return {
|
|
net: {
|
|
fetch: fetchMock,
|
|
},
|
|
protocol: {
|
|
handle: handleProtocolMock,
|
|
},
|
|
Session: jest.fn(),
|
|
};
|
|
});
|
|
|
|
import handleCustomProtocols from './handleCustomProtocols';
|
|
import { supportDir } from '@joplin/lib/testing/test-utils';
|
|
import { join } from 'path';
|
|
import { stat } from 'fs-extra';
|
|
import { toForwardSlashes } from '@joplin/utils/path';
|
|
|
|
const setUpProtocolHandler = () => {
|
|
const mockSession = {
|
|
protocol: {
|
|
handle: handleProtocolMock,
|
|
},
|
|
};
|
|
const protocolHandler = handleCustomProtocols(mockSession as never);
|
|
|
|
expect(handleProtocolMock).toHaveBeenCalled();
|
|
|
|
let onRequestListener;
|
|
for (const call of handleProtocolMock.mock.calls) {
|
|
if (call[0] === 'joplin-content') {
|
|
// The request listener is the second argument:
|
|
onRequestListener = call[1];
|
|
}
|
|
}
|
|
|
|
// Should have registered the protocol
|
|
expect(onRequestListener).toBeDefined();
|
|
|
|
return {
|
|
appProtocolHandler: protocolHandler.appContent,
|
|
onRequestListener,
|
|
};
|
|
};
|
|
|
|
interface ExpectBlockedOptions {
|
|
host?: string;
|
|
}
|
|
|
|
// Although none of the paths in this test suite point to real files, file paths must be in
|
|
// a certain format on Windows to avoid invalid path exceptions.
|
|
const toPlatformPath = (path: string) => process.platform === 'win32' ? `C:/${path}` : path;
|
|
|
|
const toAccessUrl = (path: string, { host = 'note-viewer' }: ExpectBlockedOptions = {}) => {
|
|
return `joplin-content://${host}/${toPlatformPath(path)}`;
|
|
};
|
|
|
|
const expectPathToBeBlocked = async (onRequestListener: ProtocolHandler, filePath: string, options?: ExpectBlockedOptions) => {
|
|
const url = toAccessUrl(filePath, options);
|
|
const response = await onRequestListener(new Request(url));
|
|
expect(response.status).toBe(403); // Forbidden
|
|
};
|
|
|
|
const expectPathToBeUnblocked = async (onRequestListener: ProtocolHandler, filePath: string, options?: ExpectBlockedOptions) => {
|
|
const url = toAccessUrl(filePath, options);
|
|
const handleRequestResult = await onRequestListener(
|
|
new Request(url),
|
|
);
|
|
expect(handleRequestResult.body).toBeTruthy();
|
|
};
|
|
|
|
|
|
describe('handleCustomProtocols.content', () => {
|
|
beforeEach(() => {
|
|
// Reset mocks between tests to ensure a clean testing environment.
|
|
customProtocols.clear();
|
|
handleProtocolMock.mockClear();
|
|
fetchMock.mockClear();
|
|
});
|
|
|
|
test('should only allow access to files in allowed directories', async () => {
|
|
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
|
|
|
await expectPathToBeBlocked(onRequestListener, '/test/path');
|
|
await expectPathToBeBlocked(onRequestListener, '/');
|
|
|
|
protocolHandler.allowReadAccessToDirectory(toPlatformPath('/test/path/'));
|
|
await expectPathToBeUnblocked(onRequestListener, '/test/path');
|
|
await expectPathToBeUnblocked(onRequestListener, '/test/path/a.txt');
|
|
await expectPathToBeUnblocked(onRequestListener, '/test/path/b.txt');
|
|
|
|
await expectPathToBeBlocked(onRequestListener, '/');
|
|
await expectPathToBeBlocked(onRequestListener, '/test/path2');
|
|
await expectPathToBeBlocked(onRequestListener, '/test/path/../a.txt');
|
|
|
|
protocolHandler.allowReadAccessToDirectory(toPlatformPath('/another/path/here'));
|
|
|
|
await expectPathToBeBlocked(onRequestListener, '/another/path/here2');
|
|
await expectPathToBeUnblocked(onRequestListener, '/another/path/here');
|
|
await expectPathToBeUnblocked(onRequestListener, '/another/path/here/2');
|
|
});
|
|
|
|
test('should be possible to allow and remove read access for a file', async () => {
|
|
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
|
await expectPathToBeBlocked(onRequestListener, '/test/path/a.txt');
|
|
|
|
const handle1 = protocolHandler.allowReadAccessToFile('/test/path/a.txt');
|
|
await expectPathToBeUnblocked(onRequestListener, '/test/path/a.txt');
|
|
const handle2 = protocolHandler.allowReadAccessToFile('/test/path/a.txt');
|
|
await expectPathToBeUnblocked(onRequestListener, '/test/path/a.txt');
|
|
handle1.remove();
|
|
await expectPathToBeUnblocked(onRequestListener, '/test/path/a.txt');
|
|
handle2.remove();
|
|
|
|
await expectPathToBeBlocked(onRequestListener, '/test/path/a.txt');
|
|
});
|
|
|
|
test('should only allow access to file-media/ URLs when given the correct access key', async () => {
|
|
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
|
const expectBlocked = (path: string) => {
|
|
return expectPathToBeBlocked(onRequestListener, path, { host: 'file-media' });
|
|
};
|
|
const expectUnblocked = (path: string) => {
|
|
return expectPathToBeUnblocked(onRequestListener, path, { host: 'file-media' });
|
|
};
|
|
|
|
fetchMock.mockImplementation(async (_url: string) => {
|
|
return new Response('', { headers: { 'Content-Type': 'image/jpeg' } });
|
|
});
|
|
|
|
|
|
const testPath = join(supportDir, 'photo.jpg');
|
|
await expectBlocked(testPath);
|
|
await expectBlocked(`${testPath}?access-key=wrongKey`);
|
|
await expectBlocked(`${testPath}?access-key=false`);
|
|
|
|
protocolHandler.setMediaAccessEnabled(true);
|
|
const key = protocolHandler.getMediaAccessKey();
|
|
await expectUnblocked(`${testPath}?access-key=${key}`);
|
|
await expectBlocked(`${testPath}?access-key=null`);
|
|
protocolHandler.setMediaAccessEnabled(false);
|
|
|
|
await expectBlocked(`${testPath}?access-key=${key}`);
|
|
});
|
|
|
|
test('should allow requesting part of a file', async () => {
|
|
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
|
|
|
protocolHandler.allowReadAccessToDirectory(`${supportDir}/`);
|
|
const targetFilePath = join(supportDir, 'photo.jpg');
|
|
const targetUrl = `joplin-content://note-viewer/${toForwardSlashes(targetFilePath)}`;
|
|
|
|
// Should return correct headers for an in-range response,
|
|
let response = await onRequestListener(new Request(
|
|
targetUrl,
|
|
{ headers: { 'Range': 'bytes=10-20' } },
|
|
));
|
|
|
|
expect(response.status).toBe(206); // Partial content
|
|
expect(response.headers.get('Accept-Ranges')).toBe('bytes');
|
|
expect(response.headers.get('Content-Type')).toBe('image/jpeg');
|
|
expect(response.headers.get('Content-Length')).toBe('11');
|
|
const targetStats = await stat(targetFilePath);
|
|
expect(response.headers.get('Content-Range')).toBe(`bytes 10-20/${targetStats.size}`);
|
|
|
|
// Should return correct headers for an out-of-range response,
|
|
response = await onRequestListener(new Request(
|
|
targetUrl,
|
|
{ headers: { 'Range': 'bytes=99999999999999-999999999999990' } },
|
|
));
|
|
expect(response.status).toBe(416); // Out of range
|
|
});
|
|
});
|