Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions src/operator/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,9 @@ export function plain (value, fallback = '—') {
return value === null || value === undefined || value === '' ? fallback : escapeHtml(value)
}

// The Mirall "M" and its orange square, from docs/media/logo-*.svg, on a dark tile
// so it reads on light and dark tab strips alike. Inlined as a data: URI rather
// than served: both pages live under different prefixes behind platform proxies,
// MIRALL_RELAY_ADMIN_UI=false removes the status page's routes, and the CSP
// already allows data: images.
const FAVICON_SVG = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>" +
"<rect width='64' height='64' rx='14' fill='#15181b'/>" +
"<path fill='#fff' transform='translate(37.04 48.4) scale(.175)' d='M0,-196.22L-62.292,-111.171L-126.424,-196.22L-165.931,-196.22L-165.931,0L-125.107,0L-125.107,-129.2L-65.846,-50.043L-60.578,-50.043L-1.317,-126.442L-1.317,0L39.507,0L39.507,-196.22L0,-196.22Z'/>" +
"<rect x='48' y='41.4' width='7' height='7' fill='#fb9c43'/>" +
'</svg>'

export const FAVICON_LINK = `<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}">`
// The blind-relay mark, the same artwork the StartOS package ships as its icon.
// A relative href on purpose: both pages sit behind platform proxies under
// prefixes they cannot know, and each serves its own copy (icon.png beside the
// status page, admin/icon.png beside the members page) so MIRALL_RELAY_ADMIN_UI=false
// does not take the members page's icon away with the status page's routes.
export const FAVICON_LINK = '<link rel="icon" type="image/png" href="icon.png">'
Binary file added src/operator/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/operator/members/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const ASSETS = {
// MIRALL_RELAY_ADMIN_UI=false takes it away.
'/admin/copy-button.js': [assetPath('copy-button.js'), 'text/javascript; charset=utf-8'],
// Shared with the status page's server render, so both pages name the mode alike.
'/admin/access-copy.js': [assetPath('access-copy.js'), 'text/javascript; charset=utf-8']
'/admin/access-copy.js': [assetPath('access-copy.js'), 'text/javascript; charset=utf-8'],
'/admin/icon.png': [assetPath('icon.png'), 'image/png']
}

export const PAGE_PATH = '/admin/'
Expand Down
3 changes: 2 additions & 1 deletion src/operator/status/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const ASSETS = {
'/ui.css': [assetPath('status', 'status.css'), 'text/css; charset=utf-8'],
'/ui.js': [assetPath('status', 'refresh.client.js'), 'text/javascript; charset=utf-8'],
'/format.js': [assetPath('format.js'), 'text/javascript; charset=utf-8'],
'/copy-button.js': [assetPath('copy-button.js'), 'text/javascript; charset=utf-8']
'/copy-button.js': [assetPath('copy-button.js'), 'text/javascript; charset=utf-8'],
'/icon.png': [assetPath('icon.png'), 'image/png']
}

// Every path the browser surface owns — the set the Host guard covers, and the
Expand Down
11 changes: 11 additions & 0 deletions test/integration/admin-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,14 @@ test('minting the first member moves the served pill off "no members"', async (t
const out = await request(urlOf(relay, '/'))
assert.match(out.body, /Private relay · 1 member</)
})

test('the tab icon is served beside each page, as a PNG', async (t) => {
const relay = await withHttpRelay(t)
for (const path of ['/icon.png', '/admin/icon.png']) {
const res = await fetch(urlOf(relay, path))
assert.equal(res.status, 200, path)
assert.equal(res.headers.get('content-type'), 'image/png')
const body = Buffer.from(await res.arrayBuffer())
assert.deepEqual([...body.subarray(0, 4)], [0x89, 0x50, 0x4e, 0x47], 'the bytes survive the trip, unmangled')
}
})
2 changes: 1 addition & 1 deletion test/smoke/docker-smoke.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ test('the image starts, serves /healthz and derives the expected identity', { sk
const page = await (await fetch(`http://127.0.0.1:${ADMIN_PORT}/`)).text()
assert.ok(page.includes(expected), 'the key the image derived must be on the page')
assert.match(page, /Settings → Network/)
for (const asset of ['ui.css', 'ui.js', 'format.js', 'copy-button.js']) {
for (const asset of ['ui.css', 'ui.js', 'format.js', 'copy-button.js', 'icon.png']) {
const res = await fetch(`http://127.0.0.1:${ADMIN_PORT}/${asset}`)
assert.equal(res.status, 200, `${asset} must be in the image`)
}
Expand Down
25 changes: 14 additions & 11 deletions test/unit/admin-ui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,16 +404,19 @@ test('the hero headline carries the mode at the same weight as the verdict', ()
assert.ok(!hero.includes('class="pill'), 'and the hero no longer needs a pill of its own')
})

test('both pages carry the same real icon, inline', async () => {
test('both pages carry the same icon, and each serves its own copy', async () => {
// An empty data: icon silenced the favicon request but left the tab blank.
const { renderManagePage } = await import('../../src/operator/members/page.js')
const icon = /<link rel="icon" type="image\/svg\+xml" href="(data:image\/svg\+xml,[^"]+)">/
const onStatus = renderPage(status()).match(icon)
const onMembers = renderManagePage().match(icon)
assert.ok(onStatus && onMembers, 'each page declares an svg icon')
assert.equal(onStatus[1], onMembers[1])
const svg = decodeURIComponent(onStatus[1].slice('data:image/svg+xml,'.length))
assert.match(svg, /^<svg xmlns=/)
assert.match(svg, /#fb9c43/, 'the brand orange')
assert.ok(!onStatus[1].includes('"') && !onStatus[1].includes('<'), 'safe inside a double-quoted attribute')
const { renderManagePage, managePaths } = await import('../../src/operator/members/page.js')
const icon = '<link rel="icon" type="image/png" href="icon.png">'
assert.ok(renderPage(status()).includes(icon), 'relative, so a proxy prefix works')
assert.ok(renderManagePage().includes(icon))

const onStatus = loadAssets().get('/icon.png')
const onMembers = managePaths().get('/admin/icon.png')
assert.ok(onStatus && onMembers, 'MIRALL_RELAY_ADMIN_UI=false must not take the members icon away')
assert.equal(onStatus.type, 'image/png')
assert.ok(onStatus.body.equals(onMembers.body))
assert.deepEqual([...onStatus.body.subarray(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 'a real PNG')
assert.ok(onStatus.body.length < 16 * 1024, 'small enough to be a tab icon, not the full artwork')
assert.ok(uiPaths.has('/icon.png'), 'and covered by the Host guard like the other ui paths')
})
Loading