github-issue-tower-defence-management 1.128.0 → 1.128.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CufymFM1.js +103 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/bin/adapter/entry-points/console/webServer.js +56 -3
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/content/ConsoleMarkdownContent.stories.tsx +1 -2
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +2 -4
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +7 -7
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +15 -18
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +2 -5
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +22 -25
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +12 -23
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.test.ts +7 -10
- package/src/adapter/entry-points/console/ui/src/features/console/lib/imageProxy.ts +2 -9
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +2 -4
- package/src/adapter/entry-points/console/ui-dist/assets/index-CufymFM1.js +103 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +283 -6
- package/src/adapter/entry-points/console/webServer.ts +64 -0
- package/types/adapter/entry-points/console/webServer.d.ts +5 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-8H0HejYa.js +0 -103
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +0 -41
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +0 -64
- package/src/adapter/entry-points/console/ui-dist/assets/index-8H0HejYa.js +0 -103
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CufymFM1.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-N9M1qhkw.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -6,11 +6,13 @@ import { mock } from 'jest-mock-extended';
|
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_WEB_PORT,
|
|
8
8
|
CONSOLE_TOKEN_HEADER,
|
|
9
|
+
CONSOLE_TOKEN_COOKIE,
|
|
9
10
|
hasDotSegment,
|
|
10
11
|
requiresToken,
|
|
11
12
|
isTokenValid,
|
|
12
13
|
isConsoleAppRoute,
|
|
13
14
|
extractProvidedToken,
|
|
15
|
+
extractCookieToken,
|
|
14
16
|
resolveFlatInTmuxFilePath,
|
|
15
17
|
resolveDashboardFilePath,
|
|
16
18
|
startWebServer,
|
|
@@ -109,16 +111,53 @@ describe('webServer pure helpers', () => {
|
|
|
109
111
|
|
|
110
112
|
describe('extractProvidedToken', () => {
|
|
111
113
|
it('prefers the query token', () => {
|
|
112
|
-
expect(
|
|
114
|
+
expect(
|
|
115
|
+
extractProvidedToken('fromQuery', 'fromHeader', 'fromCookie'),
|
|
116
|
+
).toBe('fromQuery');
|
|
113
117
|
});
|
|
114
118
|
|
|
115
|
-
it('falls back to the header token', () => {
|
|
116
|
-
expect(extractProvidedToken(null, 'fromHeader')).toBe(
|
|
119
|
+
it('falls back to the header token before the cookie token', () => {
|
|
120
|
+
expect(extractProvidedToken(null, 'fromHeader', 'fromCookie')).toBe(
|
|
121
|
+
'fromHeader',
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('falls back to the cookie token when query and header are absent', () => {
|
|
126
|
+
expect(extractProvidedToken(null, undefined, 'fromCookie')).toBe(
|
|
127
|
+
'fromCookie',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns null when none is present', () => {
|
|
132
|
+
expect(extractProvidedToken(null, undefined, null)).toBeNull();
|
|
133
|
+
expect(extractProvidedToken('', undefined, '')).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('extractCookieToken', () => {
|
|
138
|
+
it('reads the token cookie value from a cookie header', () => {
|
|
139
|
+
expect(extractCookieToken(`${CONSOLE_TOKEN_COOKIE}=cookie-value`)).toBe(
|
|
140
|
+
'cookie-value',
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('reads the token cookie when other cookies are present', () => {
|
|
145
|
+
expect(
|
|
146
|
+
extractCookieToken(
|
|
147
|
+
`other=1; ${CONSOLE_TOKEN_COOKIE}=cookie-value; a=b`,
|
|
148
|
+
),
|
|
149
|
+
).toBe('cookie-value');
|
|
117
150
|
});
|
|
118
151
|
|
|
119
|
-
it('
|
|
120
|
-
expect(
|
|
121
|
-
|
|
152
|
+
it('decodes a percent-encoded cookie value', () => {
|
|
153
|
+
expect(extractCookieToken(`${CONSOLE_TOKEN_COOKIE}=a%20b`)).toBe('a b');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns null when the token cookie is absent, empty, or undefined', () => {
|
|
157
|
+
expect(extractCookieToken('other=1')).toBeNull();
|
|
158
|
+
expect(extractCookieToken(`${CONSOLE_TOKEN_COOKIE}=`)).toBeNull();
|
|
159
|
+
expect(extractCookieToken(undefined)).toBeNull();
|
|
160
|
+
expect(extractCookieToken('')).toBeNull();
|
|
122
161
|
});
|
|
123
162
|
});
|
|
124
163
|
});
|
|
@@ -1618,3 +1657,241 @@ describe('webServer image proxy', () => {
|
|
|
1618
1657
|
}
|
|
1619
1658
|
});
|
|
1620
1659
|
});
|
|
1660
|
+
|
|
1661
|
+
describe('webServer token cookie redirect', () => {
|
|
1662
|
+
const testToken = 'integration-test-token-value';
|
|
1663
|
+
|
|
1664
|
+
const closeServer = (server: http.Server): Promise<void> =>
|
|
1665
|
+
new Promise((resolve, reject) => {
|
|
1666
|
+
server.close((error) => {
|
|
1667
|
+
if (error) {
|
|
1668
|
+
reject(error);
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
resolve();
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
const requestServer = (
|
|
1676
|
+
server: http.Server,
|
|
1677
|
+
requestPath: string,
|
|
1678
|
+
headers: http.OutgoingHttpHeaders = {},
|
|
1679
|
+
): Promise<{
|
|
1680
|
+
statusCode: number;
|
|
1681
|
+
body: string;
|
|
1682
|
+
location: string | undefined;
|
|
1683
|
+
setCookie: string[] | undefined;
|
|
1684
|
+
referrerPolicy: string | string[] | undefined;
|
|
1685
|
+
cacheControl: string | undefined;
|
|
1686
|
+
}> => {
|
|
1687
|
+
const address = server.address();
|
|
1688
|
+
if (address === null || typeof address === 'string') {
|
|
1689
|
+
throw new Error('server is not listening on a TCP port');
|
|
1690
|
+
}
|
|
1691
|
+
const port = address.port;
|
|
1692
|
+
return new Promise((resolve, reject) => {
|
|
1693
|
+
const httpRequest = http.request(
|
|
1694
|
+
{ host: '127.0.0.1', port, path: requestPath, headers },
|
|
1695
|
+
(response) => {
|
|
1696
|
+
const chunks: Uint8Array[] = [];
|
|
1697
|
+
response.on('data', (chunk: Uint8Array) => chunks.push(chunk));
|
|
1698
|
+
response.on('end', () => {
|
|
1699
|
+
resolve({
|
|
1700
|
+
statusCode: response.statusCode ?? 0,
|
|
1701
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
1702
|
+
location: response.headers['location'],
|
|
1703
|
+
setCookie: response.headers['set-cookie'],
|
|
1704
|
+
referrerPolicy: response.headers['referrer-policy'],
|
|
1705
|
+
cacheControl: response.headers['cache-control'],
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
},
|
|
1709
|
+
);
|
|
1710
|
+
httpRequest.on('error', reject);
|
|
1711
|
+
httpRequest.end();
|
|
1712
|
+
});
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
const startWithUiDist = async (): Promise<{
|
|
1716
|
+
server: http.Server;
|
|
1717
|
+
tmpDir: string;
|
|
1718
|
+
}> => {
|
|
1719
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1720
|
+
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
1721
|
+
fs.mkdirSync(uiDistDir, { recursive: true });
|
|
1722
|
+
fs.writeFileSync(
|
|
1723
|
+
path.join(uiDistDir, 'index.html'),
|
|
1724
|
+
'<!DOCTYPE html><title>spa</title><div id="root"></div>',
|
|
1725
|
+
);
|
|
1726
|
+
const server = await startWebServer({
|
|
1727
|
+
accessToken: testToken,
|
|
1728
|
+
uiDistDir,
|
|
1729
|
+
consoleDataOutputDir: null,
|
|
1730
|
+
inTmuxDataDir: null,
|
|
1731
|
+
dashboardDir: null,
|
|
1732
|
+
dashboardDataDir: null,
|
|
1733
|
+
dashboardProjectNames: [],
|
|
1734
|
+
port: 0,
|
|
1735
|
+
});
|
|
1736
|
+
return { server, tmpDir };
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1739
|
+
const firstSetCookie = (setCookie: string[] | undefined): string => {
|
|
1740
|
+
expect(setCookie).toBeDefined();
|
|
1741
|
+
expect(setCookie).toHaveLength(1);
|
|
1742
|
+
return (setCookie ?? [''])[0];
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
it('redirects a per-project app route carrying ?k= to the keyless path and sets an HttpOnly SameSite=Strict cookie', async () => {
|
|
1746
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1747
|
+
try {
|
|
1748
|
+
const response = await requestServer(
|
|
1749
|
+
server,
|
|
1750
|
+
`/projects/umino?k=${testToken}`,
|
|
1751
|
+
);
|
|
1752
|
+
expect(response.statusCode).toBe(302);
|
|
1753
|
+
expect(response.location).toBe('/projects/umino');
|
|
1754
|
+
expect(response.body).not.toContain(testToken);
|
|
1755
|
+
const cookie = firstSetCookie(response.setCookie);
|
|
1756
|
+
expect(cookie).toContain(`${CONSOLE_TOKEN_COOKIE}=${testToken}`);
|
|
1757
|
+
expect(cookie).toContain('HttpOnly');
|
|
1758
|
+
expect(cookie).toContain('SameSite=Strict');
|
|
1759
|
+
expect(cookie).toContain('Path=/');
|
|
1760
|
+
expect(response.referrerPolicy).toBe('no-referrer');
|
|
1761
|
+
} finally {
|
|
1762
|
+
await closeServer(server);
|
|
1763
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
it('redirects a per-project tab route carrying ?k= and preserves other query parameters', async () => {
|
|
1768
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1769
|
+
try {
|
|
1770
|
+
const response = await requestServer(
|
|
1771
|
+
server,
|
|
1772
|
+
`/projects/xmile/prs?k=${testToken}&foo=bar`,
|
|
1773
|
+
);
|
|
1774
|
+
expect(response.statusCode).toBe(302);
|
|
1775
|
+
expect(response.location).toBe('/projects/xmile/prs?foo=bar');
|
|
1776
|
+
const cookie = firstSetCookie(response.setCookie);
|
|
1777
|
+
expect(cookie).toContain(`${CONSOLE_TOKEN_COOKIE}=${testToken}`);
|
|
1778
|
+
} finally {
|
|
1779
|
+
await closeServer(server);
|
|
1780
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
it('redirects the root and index.html routes carrying ?k= to a keyless path', async () => {
|
|
1785
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1786
|
+
try {
|
|
1787
|
+
const root = await requestServer(server, `/?k=${testToken}`);
|
|
1788
|
+
expect(root.statusCode).toBe(302);
|
|
1789
|
+
expect(root.location).toBe('/');
|
|
1790
|
+
expect(firstSetCookie(root.setCookie)).toContain(
|
|
1791
|
+
`${CONSOLE_TOKEN_COOKIE}=${testToken}`,
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
const indexHtml = await requestServer(
|
|
1795
|
+
server,
|
|
1796
|
+
`/index.html?k=${testToken}`,
|
|
1797
|
+
);
|
|
1798
|
+
expect(indexHtml.statusCode).toBe(302);
|
|
1799
|
+
expect(indexHtml.location).toBe('/index.html');
|
|
1800
|
+
} finally {
|
|
1801
|
+
await closeServer(server);
|
|
1802
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
it('serves the app route without a token and never leaks a token in the URL', async () => {
|
|
1807
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1808
|
+
try {
|
|
1809
|
+
const response = await requestServer(server, '/projects/umino');
|
|
1810
|
+
expect(response.statusCode).toBe(200);
|
|
1811
|
+
expect(response.body).toContain('spa');
|
|
1812
|
+
expect(response.setCookie).toBeUndefined();
|
|
1813
|
+
expect(response.referrerPolicy).toBe('no-referrer');
|
|
1814
|
+
} finally {
|
|
1815
|
+
await closeServer(server);
|
|
1816
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
it('sets Referrer-Policy: no-referrer on the console HTML responses', async () => {
|
|
1821
|
+
const { server, tmpDir } = await startWithUiDist();
|
|
1822
|
+
try {
|
|
1823
|
+
const root = await requestServer(server, '/');
|
|
1824
|
+
expect(root.statusCode).toBe(200);
|
|
1825
|
+
expect(root.referrerPolicy).toBe('no-referrer');
|
|
1826
|
+
|
|
1827
|
+
const projectTab = await requestServer(server, '/projects/xmile/prs');
|
|
1828
|
+
expect(projectTab.statusCode).toBe(200);
|
|
1829
|
+
expect(projectTab.referrerPolicy).toBe('no-referrer');
|
|
1830
|
+
} finally {
|
|
1831
|
+
await closeServer(server);
|
|
1832
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
it('authenticates a data list request through the cookie without a token in the URL', async () => {
|
|
1837
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1838
|
+
const dataDir = path.join(tmpDir, 'data');
|
|
1839
|
+
const listDir = path.join(dataDir, 'umino', 'prs');
|
|
1840
|
+
fs.mkdirSync(listDir, { recursive: true });
|
|
1841
|
+
fs.writeFileSync(
|
|
1842
|
+
path.join(listDir, 'list.json'),
|
|
1843
|
+
JSON.stringify({ pjcode: 'umino', items: [] }),
|
|
1844
|
+
);
|
|
1845
|
+
const server = await startWebServer({
|
|
1846
|
+
accessToken: testToken,
|
|
1847
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1848
|
+
consoleDataOutputDir: dataDir,
|
|
1849
|
+
inTmuxDataDir: null,
|
|
1850
|
+
dashboardDir: null,
|
|
1851
|
+
dashboardDataDir: null,
|
|
1852
|
+
dashboardProjectNames: [],
|
|
1853
|
+
port: 0,
|
|
1854
|
+
});
|
|
1855
|
+
try {
|
|
1856
|
+
const withCookie = await requestServer(
|
|
1857
|
+
server,
|
|
1858
|
+
'/projects/umino/prs/list.json',
|
|
1859
|
+
{ Cookie: `${CONSOLE_TOKEN_COOKIE}=${testToken}` },
|
|
1860
|
+
);
|
|
1861
|
+
expect(withCookie.statusCode).toBe(200);
|
|
1862
|
+
expect(JSON.parse(withCookie.body)).toEqual({
|
|
1863
|
+
pjcode: 'umino',
|
|
1864
|
+
items: [],
|
|
1865
|
+
});
|
|
1866
|
+
} finally {
|
|
1867
|
+
await closeServer(server);
|
|
1868
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
it('rejects a data or api request when no token and no cookie are present', async () => {
|
|
1873
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1874
|
+
const server = await startWebServer({
|
|
1875
|
+
accessToken: testToken,
|
|
1876
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
1877
|
+
consoleDataOutputDir: null,
|
|
1878
|
+
inTmuxDataDir: null,
|
|
1879
|
+
dashboardDir: null,
|
|
1880
|
+
dashboardDataDir: null,
|
|
1881
|
+
dashboardProjectNames: [],
|
|
1882
|
+
port: 0,
|
|
1883
|
+
});
|
|
1884
|
+
try {
|
|
1885
|
+
const noToken = await requestServer(server, '/data/situation.json');
|
|
1886
|
+
expect(noToken.statusCode).toBe(401);
|
|
1887
|
+
|
|
1888
|
+
const wrongCookie = await requestServer(server, '/api/review', {
|
|
1889
|
+
Cookie: `${CONSOLE_TOKEN_COOKIE}=wrong-token`,
|
|
1890
|
+
});
|
|
1891
|
+
expect(wrongCookie.statusCode).toBe(401);
|
|
1892
|
+
} finally {
|
|
1893
|
+
await closeServer(server);
|
|
1894
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
@@ -41,6 +41,8 @@ export const DEFAULT_DASHBOARD_PROJECT_NAMES = DASHBOARD_PROJECT_NAMES;
|
|
|
41
41
|
|
|
42
42
|
export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
|
|
43
43
|
|
|
44
|
+
export const CONSOLE_TOKEN_COOKIE = 'pv_token';
|
|
45
|
+
|
|
44
46
|
const PLACEHOLDER_INDEX_HTML = `<!DOCTYPE html>
|
|
45
47
|
<html lang="en">
|
|
46
48
|
<head>
|
|
@@ -112,9 +114,34 @@ export const isTokenValid = (
|
|
|
112
114
|
providedToken: string | null,
|
|
113
115
|
): boolean => providedToken !== null && providedToken === expectedToken;
|
|
114
116
|
|
|
117
|
+
export const extractCookieToken = (
|
|
118
|
+
cookieHeader: string | undefined,
|
|
119
|
+
): string | null => {
|
|
120
|
+
if (typeof cookieHeader !== 'string' || cookieHeader.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
for (const segment of cookieHeader.split(';')) {
|
|
124
|
+
const separatorIndex = segment.indexOf('=');
|
|
125
|
+
if (separatorIndex === -1) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const name = segment.slice(0, separatorIndex).trim();
|
|
129
|
+
if (name !== CONSOLE_TOKEN_COOKIE) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const value = segment.slice(separatorIndex + 1).trim();
|
|
133
|
+
if (value.length === 0) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return decodeURIComponent(value);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
|
|
115
141
|
export const extractProvidedToken = (
|
|
116
142
|
queryToken: string | string[] | null,
|
|
117
143
|
headerToken: string | string[] | undefined,
|
|
144
|
+
cookieToken: string | null,
|
|
118
145
|
): string | null => {
|
|
119
146
|
if (typeof queryToken === 'string' && queryToken.length > 0) {
|
|
120
147
|
return queryToken;
|
|
@@ -122,9 +149,24 @@ export const extractProvidedToken = (
|
|
|
122
149
|
if (typeof headerToken === 'string' && headerToken.length > 0) {
|
|
123
150
|
return headerToken;
|
|
124
151
|
}
|
|
152
|
+
if (cookieToken !== null && cookieToken.length > 0) {
|
|
153
|
+
return cookieToken;
|
|
154
|
+
}
|
|
125
155
|
return null;
|
|
126
156
|
};
|
|
127
157
|
|
|
158
|
+
export const buildTokenCookie = (token: string): string =>
|
|
159
|
+
`${CONSOLE_TOKEN_COOKIE}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Strict`;
|
|
160
|
+
|
|
161
|
+
export const buildKeylessLocation = (requestUrl: URL): string => {
|
|
162
|
+
const params = new URLSearchParams(requestUrl.searchParams);
|
|
163
|
+
params.delete('k');
|
|
164
|
+
const query = params.toString();
|
|
165
|
+
return query.length > 0
|
|
166
|
+
? `${requestUrl.pathname}?${query}`
|
|
167
|
+
: requestUrl.pathname;
|
|
168
|
+
};
|
|
169
|
+
|
|
128
170
|
const contentTypeForPath = (filePath: string): string => {
|
|
129
171
|
const extension = path.extname(filePath).toLowerCase();
|
|
130
172
|
return MIME_TYPES[extension] ?? 'application/octet-stream';
|
|
@@ -245,6 +287,7 @@ const serveBootstrapIndex = (response: http.ServerResponse): void => {
|
|
|
245
287
|
response.writeHead(200, {
|
|
246
288
|
'Content-Type': 'text/html; charset=utf-8',
|
|
247
289
|
'Cache-Control': 'no-store',
|
|
290
|
+
'Referrer-Policy': 'no-referrer',
|
|
248
291
|
});
|
|
249
292
|
response.end(PLACEHOLDER_INDEX_HTML);
|
|
250
293
|
};
|
|
@@ -263,10 +306,25 @@ const serveIndexHtml = (
|
|
|
263
306
|
response.writeHead(200, {
|
|
264
307
|
'Content-Type': 'text/html; charset=utf-8',
|
|
265
308
|
'Cache-Control': 'no-store',
|
|
309
|
+
'Referrer-Policy': 'no-referrer',
|
|
266
310
|
});
|
|
267
311
|
response.end(indexContent);
|
|
268
312
|
};
|
|
269
313
|
|
|
314
|
+
const redirectStrippingToken = (
|
|
315
|
+
response: http.ServerResponse,
|
|
316
|
+
requestUrl: URL,
|
|
317
|
+
token: string,
|
|
318
|
+
): void => {
|
|
319
|
+
response.writeHead(302, {
|
|
320
|
+
Location: buildKeylessLocation(requestUrl),
|
|
321
|
+
'Set-Cookie': buildTokenCookie(token),
|
|
322
|
+
'Referrer-Policy': 'no-referrer',
|
|
323
|
+
'Cache-Control': 'no-store',
|
|
324
|
+
});
|
|
325
|
+
response.end();
|
|
326
|
+
};
|
|
327
|
+
|
|
270
328
|
const sendJson = (
|
|
271
329
|
response: http.ServerResponse,
|
|
272
330
|
statusCode: number,
|
|
@@ -626,6 +684,7 @@ export const handleWebRequest = async (
|
|
|
626
684
|
const providedToken = extractProvidedToken(
|
|
627
685
|
requestUrl.searchParams.get('k'),
|
|
628
686
|
request.headers[CONSOLE_TOKEN_HEADER],
|
|
687
|
+
extractCookieToken(request.headers.cookie),
|
|
629
688
|
);
|
|
630
689
|
if (!isTokenValid(options.accessToken, providedToken)) {
|
|
631
690
|
sendUnauthorized(response);
|
|
@@ -646,6 +705,11 @@ export const handleWebRequest = async (
|
|
|
646
705
|
requestPath === '/index.html' ||
|
|
647
706
|
isConsoleAppRoute(requestPath)
|
|
648
707
|
) {
|
|
708
|
+
const queryToken = requestUrl.searchParams.get('k');
|
|
709
|
+
if (queryToken !== null && queryToken.length > 0) {
|
|
710
|
+
redirectStrippingToken(response, requestUrl, queryToken);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
649
713
|
serveIndexHtml(options, response);
|
|
650
714
|
return;
|
|
651
715
|
}
|
|
@@ -6,11 +6,15 @@ import { ImageFetcher } from './consoleImageProxy';
|
|
|
6
6
|
export declare const DEFAULT_WEB_PORT = 9981;
|
|
7
7
|
export declare const DEFAULT_DASHBOARD_PROJECT_NAMES: string[];
|
|
8
8
|
export declare const CONSOLE_TOKEN_HEADER = "x-pv-token";
|
|
9
|
+
export declare const CONSOLE_TOKEN_COOKIE = "pv_token";
|
|
9
10
|
export declare const hasDotSegment: (requestPath: string) => boolean;
|
|
10
11
|
export declare const requiresToken: (requestPath: string) => boolean;
|
|
11
12
|
export declare const isConsoleAppRoute: (requestPath: string) => boolean;
|
|
12
13
|
export declare const isTokenValid: (expectedToken: string, providedToken: string | null) => boolean;
|
|
13
|
-
export declare const
|
|
14
|
+
export declare const extractCookieToken: (cookieHeader: string | undefined) => string | null;
|
|
15
|
+
export declare const extractProvidedToken: (queryToken: string | string[] | null, headerToken: string | string[] | undefined, cookieToken: string | null) => string | null;
|
|
16
|
+
export declare const buildTokenCookie: (token: string) => string;
|
|
17
|
+
export declare const buildKeylessLocation: (requestUrl: URL) => string;
|
|
14
18
|
export type WebServerOptions = {
|
|
15
19
|
accessToken: string;
|
|
16
20
|
uiDistDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/webServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,EAAE,eAAe,EAAE,MAAM,6DAA6D,CAAC;AAM9F,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EAQvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAEL,sBAAsB,EACtB,sBAAsB,EAMvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AAOtE,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,eAAO,MAAM,+BAA+B,UAA0B,CAAC;AAEvE,eAAO,MAAM,oBAAoB,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"webServer.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/console/webServer.ts"],"names":[],"mappings":"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"}
|