feat(hololake): add fail-closed JD release broadcast origin
This commit is contained in:
parent
df563a9fdd
commit
f73e11b364
6 changed files with 423 additions and 2 deletions
|
|
@ -20,8 +20,11 @@
|
||||||
"release_package_signature_size_sha256_verification_implemented": true,
|
"release_package_signature_size_sha256_verification_implemented": true,
|
||||||
"release_persistent_rollback_executor_implemented": true,
|
"release_persistent_rollback_executor_implemented": true,
|
||||||
"release_signed_notarized_pipeline_implemented": true,
|
"release_signed_notarized_pipeline_implemented": true,
|
||||||
|
"release_broadcast_candidate_service_implemented": true,
|
||||||
|
"release_broadcast_candidate_service_default_state": "EMPTY_FAIL_CLOSED_LOOPBACK_ONLY",
|
||||||
|
"release_broadcast_candidate_service_source": "server/release-broadcast/server.mjs",
|
||||||
"release_pipeline_automatic_upload_allowed": false,
|
"release_pipeline_automatic_upload_allowed": false,
|
||||||
"release_production_activation_state": "BLOCKED_PENDING_JD_TRUST_SIGNED_PIPELINE_AND_APPLE_NOTARIZATION",
|
"release_production_activation_state": "BLOCKED_PENDING_PUBLIC_HTTPS_TRUST_UPDATER_KEY_PIPELINE_EXECUTION_AND_APPLE_NOTARIZATION",
|
||||||
"tauri_update_artifacts_enabled": false,
|
"tauri_update_artifacts_enabled": false,
|
||||||
"tauri_update_artifacts_enablement_gate": "JD_CONTROLLER_PUBLIC_KEY_AND_SIGNED_RELEASE_PIPELINE_REQUIRED",
|
"tauri_update_artifacts_enablement_gate": "JD_CONTROLLER_PUBLIC_KEY_AND_SIGNED_RELEASE_PIPELINE_REQUIRED",
|
||||||
"automatic_update_check_on_startup": false,
|
"automatic_update_check_on_startup": false,
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,12 @@ test('release activation remains explicitly human controlled', () => {
|
||||||
assert.equal(foundation.release_package_signature_size_sha256_verification_implemented, true)
|
assert.equal(foundation.release_package_signature_size_sha256_verification_implemented, true)
|
||||||
assert.equal(foundation.release_persistent_rollback_executor_implemented, true)
|
assert.equal(foundation.release_persistent_rollback_executor_implemented, true)
|
||||||
assert.equal(foundation.release_signed_notarized_pipeline_implemented, true)
|
assert.equal(foundation.release_signed_notarized_pipeline_implemented, true)
|
||||||
|
assert.equal(foundation.release_broadcast_candidate_service_implemented, true)
|
||||||
|
assert.equal(foundation.release_broadcast_candidate_service_default_state, 'EMPTY_FAIL_CLOSED_LOOPBACK_ONLY')
|
||||||
assert.equal(foundation.release_pipeline_automatic_upload_allowed, false)
|
assert.equal(foundation.release_pipeline_automatic_upload_allowed, false)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
foundation.release_production_activation_state,
|
foundation.release_production_activation_state,
|
||||||
'BLOCKED_PENDING_JD_TRUST_SIGNED_PIPELINE_AND_APPLE_NOTARIZATION',
|
'BLOCKED_PENDING_PUBLIC_HTTPS_TRUST_UPDATER_KEY_PIPELINE_EXECUTION_AND_APPLE_NOTARIZATION',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { createReleaseServer, loadRuntimeState } from '../server/release-broadcast/server.mjs'
|
||||||
|
|
||||||
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
|
||||||
|
|
||||||
|
const writeJson = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
||||||
|
|
||||||
|
async function withServer(state, callback) {
|
||||||
|
const server = createReleaseServer(state)
|
||||||
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||||
|
const address = server.address()
|
||||||
|
try {
|
||||||
|
return await callback(`http://127.0.0.1:${address.port}`)
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReleaseRoot() {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-broadcast-'))
|
||||||
|
const releaseDirectory = path.join(root, 'releases', '0.2.0')
|
||||||
|
fs.mkdirSync(releaseDirectory, { recursive: true })
|
||||||
|
const packageBytes = Buffer.from('signed-notarized-updater-placeholder')
|
||||||
|
const packageName = 'HoloLake.app.tar.gz'
|
||||||
|
fs.writeFileSync(path.join(releaseDirectory, packageName), packageBytes, { mode: 0o600 })
|
||||||
|
writeJson(path.join(releaseDirectory, 'HOLOLAKE-CODESIGN.json'), {
|
||||||
|
schema: 'hololake.platform-code-signature-receipt/v1',
|
||||||
|
state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED',
|
||||||
|
})
|
||||||
|
writeJson(path.join(releaseDirectory, 'HOLOLAKE-NOTARIZATION.json'), {
|
||||||
|
schema: 'hololake.apple-notarization-receipt/v1',
|
||||||
|
state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED',
|
||||||
|
})
|
||||||
|
const broadcast = {
|
||||||
|
schema: 'hololake.release-broadcast/v1',
|
||||||
|
releaseId: 'GH-HOLOLAKE-RELEASE-0.2.0',
|
||||||
|
version: '0.2.0',
|
||||||
|
pub_date: '2026-08-13T10:00:00.000Z',
|
||||||
|
notes: 'Signed release',
|
||||||
|
platforms: {
|
||||||
|
'darwin-aarch64': {
|
||||||
|
url: `https://release.guanghu.test/releases/0.2.0/${packageName}`,
|
||||||
|
signature: 'trusted-updater-signature',
|
||||||
|
size: packageBytes.length,
|
||||||
|
sha256: sha256(packageBytes),
|
||||||
|
platformCodeSignatureReceipt: 'HOLOLAKE-CODESIGN',
|
||||||
|
notarizationReceipt: 'HOLOLAKE-NOTARIZATION',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hololake: {
|
||||||
|
features: ['Stage one'],
|
||||||
|
fixes: [],
|
||||||
|
compatibility: { minimumVersion: '0.1.0', dataMigrationRequired: false },
|
||||||
|
restart: { required: true, automaticAllowed: false },
|
||||||
|
rollback: { supported: true, healthReceiptRequired: true, previousVersion: '0.1.0' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const broadcastPath = path.join(releaseDirectory, 'latest.json')
|
||||||
|
writeJson(broadcastPath, broadcast)
|
||||||
|
const broadcastSha256 = sha256(fs.readFileSync(broadcastPath))
|
||||||
|
writeJson(path.join(releaseDirectory, 'pipeline-receipt.json'), {
|
||||||
|
schema: 'hololake.signed-release-pipeline-receipt/v1',
|
||||||
|
state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD',
|
||||||
|
broadcastSha256,
|
||||||
|
automaticUpload: false,
|
||||||
|
automaticActivation: false,
|
||||||
|
})
|
||||||
|
writeJson(path.join(root, 'ACTIVE.json'), {
|
||||||
|
schema: 'hololake.release-broadcast-activation/v1',
|
||||||
|
state: 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE',
|
||||||
|
releaseId: broadcast.releaseId,
|
||||||
|
version: broadcast.version,
|
||||||
|
broadcastRelativePath: 'releases/0.2.0/latest.json',
|
||||||
|
broadcastSha256,
|
||||||
|
pipelineReceiptRelativePath: 'releases/0.2.0/pipeline-receipt.json',
|
||||||
|
humanApprovalReceipt: 'GH-HUMAN-RELEASE-APPROVAL-001',
|
||||||
|
})
|
||||||
|
return { root, packageBytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('empty release root stays healthy but returns Tauri-compatible 204 no update', async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-empty-'))
|
||||||
|
const state = loadRuntimeState(root)
|
||||||
|
assert.equal(state.state, 'EMPTY_FAIL_CLOSED')
|
||||||
|
await withServer(state, async (base) => {
|
||||||
|
const health = await fetch(`${base}/health`)
|
||||||
|
assert.equal(health.status, 200)
|
||||||
|
assert.deepEqual((await health.json()).upstreamUpdateSources, [])
|
||||||
|
assert.equal((await fetch(`${base}/latest.json`)).status, 204)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('invalid activation locks the endpoint instead of falling back to an update', async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-invalid-'))
|
||||||
|
writeJson(path.join(root, 'ACTIVE.json'), { schema: 'wrong', state: 'ACTIVE' })
|
||||||
|
const state = loadRuntimeState(root)
|
||||||
|
assert.equal(state.state, 'LOCKED_INVALID_RELEASE_EVIDENCE')
|
||||||
|
await withServer(state, async (base) => {
|
||||||
|
assert.equal((await fetch(`${base}/health`)).status, 503)
|
||||||
|
assert.equal((await fetch(`${base}/latest.json`)).status, 503)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('only an exact human-approved signed notarized evidence chain becomes readable', async () => {
|
||||||
|
const fixture = buildReleaseRoot()
|
||||||
|
const state = loadRuntimeState(fixture.root)
|
||||||
|
assert.equal(state.state, 'READY_SIGNED_NOTARIZED_BROADCAST')
|
||||||
|
await withServer(state, async (base) => {
|
||||||
|
const latest = await fetch(`${base}/latest.json`)
|
||||||
|
assert.equal(latest.status, 200)
|
||||||
|
assert.equal((await latest.json()).releaseId, 'GH-HOLOLAKE-RELEASE-0.2.0')
|
||||||
|
const updater = await fetch(`${base}/releases/0.2.0/HoloLake.app.tar.gz`)
|
||||||
|
assert.equal(updater.status, 200)
|
||||||
|
assert.deepEqual(Buffer.from(await updater.arrayBuffer()), fixture.packageBytes)
|
||||||
|
assert.equal((await fetch(`${base}/unknown`)).status, 404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('package tampering after pipeline output locks the whole release at startup', () => {
|
||||||
|
const fixture = buildReleaseRoot()
|
||||||
|
fs.appendFileSync(path.join(fixture.root, 'releases', '0.2.0', 'HoloLake.app.tar.gz'), 'tampered')
|
||||||
|
assert.equal(loadRuntimeState(fixture.root).state, 'LOCKED_INVALID_RELEASE_EVIDENCE')
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# HoloLake release broadcast candidate
|
||||||
|
|
||||||
|
This is the HoloLake-owned, loopback-only origin for signed update broadcasts.
|
||||||
|
It has no upstream software feed and never uploads or activates a release.
|
||||||
|
|
||||||
|
Without `ACTIVE.json`, `/health` reports `EMPTY_FAIL_CLOSED` and `/latest.json`
|
||||||
|
returns HTTP 204, the updater protocol's explicit no-update result. An active
|
||||||
|
release is accepted only when the human activation record, immutable broadcast,
|
||||||
|
pipeline receipt, package bytes, Developer ID receipt, and Apple notarization
|
||||||
|
receipt form one exact evidence chain. Invalid evidence locks the whole release
|
||||||
|
endpoint until an operator fixes the evidence and explicitly restarts the
|
||||||
|
service.
|
||||||
|
|
||||||
|
The service listens only on `127.0.0.1`. Public HTTPS routing, updater trust-key
|
||||||
|
provisioning, artifact upload, activation, and desktop rollout are independent
|
||||||
|
deployment gates.
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
[Unit]
|
||||||
|
Description=HoloLake signed release broadcast candidate
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=hololake-release
|
||||||
|
Group=hololake-release
|
||||||
|
WorkingDirectory=/opt/guanghu/hololake-release-broadcast-candidate/current
|
||||||
|
Environment=HOLOLAKE_RELEASE_HOST=127.0.0.1
|
||||||
|
Environment=HOLOLAKE_RELEASE_PORT=3940
|
||||||
|
Environment=HOLOLAKE_RELEASE_STATE_ROOT=/var/lib/guanghu/hololake-release-broadcast
|
||||||
|
Environment=HOLOLAKE_RELEASE_ENFORCE_ROOT_OWNER=1
|
||||||
|
ExecStart=/usr/bin/node /opt/guanghu/hololake-release-broadcast-candidate/current/server.mjs
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
UMask=0027
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
IPAddressDeny=any
|
||||||
|
IPAddressAllow=localhost
|
||||||
|
ReadOnlyPaths=/opt/guanghu/hololake-release-broadcast-candidate
|
||||||
|
ReadOnlyPaths=/var/lib/guanghu/hololake-release-broadcast
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import crypto from 'node:crypto'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import http from 'node:http'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const MAX_CONTROL_BYTES = 1024 * 1024
|
||||||
|
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
||||||
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
||||||
|
|
||||||
|
const fail = (code) => {
|
||||||
|
throw new Error(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
|
||||||
|
|
||||||
|
const requireText = (value, code) => {
|
||||||
|
if (typeof value !== 'string' || value.trim() === '') fail(code)
|
||||||
|
return value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function trustedRegularFile(file, { enforceRootOwner, maxBytes = null }) {
|
||||||
|
const info = fs.lstatSync(file)
|
||||||
|
if (!info.isFile() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_FILE_NOT_REGULAR')
|
||||||
|
if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_FILE_WRITABLE_BY_NON_OWNER')
|
||||||
|
if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_FILE_NOT_ROOT_OWNED')
|
||||||
|
if (maxBytes !== null && info.size > maxBytes) fail('HOLOLAKE_RELEASE_CONTROL_FILE_TOO_LARGE')
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeRelative(root, value, code) {
|
||||||
|
const relative = requireText(value, code)
|
||||||
|
if (path.isAbsolute(relative) || relative.includes('\0')) fail(code)
|
||||||
|
const resolved = path.resolve(root, relative)
|
||||||
|
if (!resolved.startsWith(`${root}${path.sep}`)) fail(code)
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTrustedJson(file, options) {
|
||||||
|
trustedRegularFile(file, { ...options, maxBytes: MAX_CONTROL_BYTES })
|
||||||
|
return { bytes: fs.readFileSync(file), value: JSON.parse(fs.readFileSync(file, 'utf8')) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePlatform(platform, releaseDirectory, enforceRootOwner) {
|
||||||
|
const url = new URL(requireText(platform?.url, 'HOLOLAKE_RELEASE_PACKAGE_URL_REQUIRED'))
|
||||||
|
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
||||||
|
fail('HOLOLAKE_RELEASE_PACKAGE_URL_INVALID')
|
||||||
|
}
|
||||||
|
const packageName = decodeURIComponent(path.posix.basename(url.pathname))
|
||||||
|
if (!packageName || packageName === '.' || packageName === '..') fail('HOLOLAKE_RELEASE_PACKAGE_NAME_INVALID')
|
||||||
|
const packagePath = safeRelative(releaseDirectory, packageName, 'HOLOLAKE_RELEASE_PACKAGE_PATH_INVALID')
|
||||||
|
const packageInfo = trustedRegularFile(packagePath, { enforceRootOwner })
|
||||||
|
if (!Number.isSafeInteger(platform.size) || platform.size <= 0 || platform.size !== packageInfo.size) {
|
||||||
|
fail('HOLOLAKE_RELEASE_PACKAGE_SIZE_MISMATCH')
|
||||||
|
}
|
||||||
|
if (!SHA256_PATTERN.test(platform.sha256 || '') || sha256(fs.readFileSync(packagePath)) !== platform.sha256) {
|
||||||
|
fail('HOLOLAKE_RELEASE_PACKAGE_SHA256_MISMATCH')
|
||||||
|
}
|
||||||
|
requireText(platform.signature, 'HOLOLAKE_RELEASE_PACKAGE_SIGNATURE_REQUIRED')
|
||||||
|
const codeReceiptId = requireText(platform.platformCodeSignatureReceipt, 'HOLOLAKE_RELEASE_CODESIGN_RECEIPT_REQUIRED')
|
||||||
|
const notarizationReceiptId = requireText(platform.notarizationReceipt, 'HOLOLAKE_RELEASE_NOTARIZATION_RECEIPT_REQUIRED')
|
||||||
|
const codeReceipt = readTrustedJson(
|
||||||
|
safeRelative(releaseDirectory, `${codeReceiptId}.json`, 'HOLOLAKE_RELEASE_CODESIGN_RECEIPT_PATH_INVALID'),
|
||||||
|
{ enforceRootOwner },
|
||||||
|
).value
|
||||||
|
const notarizationReceipt = readTrustedJson(
|
||||||
|
safeRelative(releaseDirectory, `${notarizationReceiptId}.json`, 'HOLOLAKE_RELEASE_NOTARIZATION_RECEIPT_PATH_INVALID'),
|
||||||
|
{ enforceRootOwner },
|
||||||
|
).value
|
||||||
|
if (
|
||||||
|
codeReceipt?.schema !== 'hololake.platform-code-signature-receipt/v1' ||
|
||||||
|
codeReceipt?.state !== 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED'
|
||||||
|
) fail('HOLOLAKE_RELEASE_CODESIGN_RECEIPT_INVALID')
|
||||||
|
if (
|
||||||
|
notarizationReceipt?.schema !== 'hololake.apple-notarization-receipt/v1' ||
|
||||||
|
notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED'
|
||||||
|
) fail('HOLOLAKE_RELEASE_NOTARIZATION_RECEIPT_INVALID')
|
||||||
|
return { pathname: url.pathname, file: packagePath, size: packageInfo.size }
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadActiveRelease(stateRoot, enforceRootOwner) {
|
||||||
|
const activationPath = path.join(stateRoot, 'ACTIVE.json')
|
||||||
|
if (!fs.existsSync(activationPath)) {
|
||||||
|
return { state: 'EMPTY_FAIL_CLOSED', release: null, reasonCode: null }
|
||||||
|
}
|
||||||
|
const activation = readTrustedJson(activationPath, { enforceRootOwner }).value
|
||||||
|
if (
|
||||||
|
activation?.schema !== 'hololake.release-broadcast-activation/v1' ||
|
||||||
|
activation?.state !== 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE'
|
||||||
|
) fail('HOLOLAKE_RELEASE_ACTIVATION_INVALID')
|
||||||
|
const releaseId = requireText(activation.releaseId, 'HOLOLAKE_RELEASE_ID_REQUIRED')
|
||||||
|
const version = requireText(activation.version, 'HOLOLAKE_RELEASE_VERSION_REQUIRED')
|
||||||
|
if (!VERSION_PATTERN.test(version)) fail('HOLOLAKE_RELEASE_VERSION_INVALID')
|
||||||
|
if (!SHA256_PATTERN.test(activation.broadcastSha256 || '')) fail('HOLOLAKE_RELEASE_BROADCAST_SHA256_INVALID')
|
||||||
|
requireText(activation.humanApprovalReceipt, 'HOLOLAKE_RELEASE_HUMAN_APPROVAL_REQUIRED')
|
||||||
|
const broadcastPath = safeRelative(stateRoot, activation.broadcastRelativePath, 'HOLOLAKE_RELEASE_BROADCAST_PATH_INVALID')
|
||||||
|
const pipelinePath = safeRelative(stateRoot, activation.pipelineReceiptRelativePath, 'HOLOLAKE_RELEASE_PIPELINE_PATH_INVALID')
|
||||||
|
const broadcastDocument = readTrustedJson(broadcastPath, { enforceRootOwner })
|
||||||
|
const pipelineReceipt = readTrustedJson(pipelinePath, { enforceRootOwner }).value
|
||||||
|
if (sha256(broadcastDocument.bytes) !== activation.broadcastSha256) fail('HOLOLAKE_RELEASE_BROADCAST_SHA256_MISMATCH')
|
||||||
|
if (
|
||||||
|
pipelineReceipt?.schema !== 'hololake.signed-release-pipeline-receipt/v1' ||
|
||||||
|
pipelineReceipt?.state !== 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD' ||
|
||||||
|
pipelineReceipt?.broadcastSha256 !== activation.broadcastSha256 ||
|
||||||
|
pipelineReceipt?.automaticUpload !== false ||
|
||||||
|
pipelineReceipt?.automaticActivation !== false
|
||||||
|
) fail('HOLOLAKE_RELEASE_PIPELINE_RECEIPT_INVALID')
|
||||||
|
const broadcast = broadcastDocument.value
|
||||||
|
if (
|
||||||
|
broadcast?.schema !== 'hololake.release-broadcast/v1' ||
|
||||||
|
broadcast?.releaseId !== releaseId ||
|
||||||
|
broadcast?.version !== version ||
|
||||||
|
broadcast?.hololake?.restart?.required !== true ||
|
||||||
|
broadcast?.hololake?.restart?.automaticAllowed !== false ||
|
||||||
|
broadcast?.hololake?.rollback?.supported !== true ||
|
||||||
|
broadcast?.hololake?.rollback?.healthReceiptRequired !== true ||
|
||||||
|
!broadcast.platforms || typeof broadcast.platforms !== 'object' || Array.isArray(broadcast.platforms) ||
|
||||||
|
Object.keys(broadcast.platforms).length === 0
|
||||||
|
) fail('HOLOLAKE_RELEASE_BROADCAST_INVALID')
|
||||||
|
const releaseDirectory = path.dirname(broadcastPath)
|
||||||
|
const packages = new Map()
|
||||||
|
for (const platform of Object.values(broadcast.platforms)) {
|
||||||
|
const accepted = validatePlatform(platform, releaseDirectory, enforceRootOwner)
|
||||||
|
if (packages.has(accepted.pathname)) fail('HOLOLAKE_RELEASE_PACKAGE_ROUTE_COLLISION')
|
||||||
|
packages.set(accepted.pathname, accepted)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
state: 'READY_SIGNED_NOTARIZED_BROADCAST',
|
||||||
|
release: { releaseId, version, broadcastBytes: broadcastDocument.bytes, packages },
|
||||||
|
reasonCode: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadRuntimeState(stateRoot, { enforceRootOwner = false } = {}) {
|
||||||
|
const resolvedRoot = path.resolve(stateRoot)
|
||||||
|
try {
|
||||||
|
return loadActiveRelease(resolvedRoot, enforceRootOwner)
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
state: 'LOCKED_INVALID_RELEASE_EVIDENCE',
|
||||||
|
release: null,
|
||||||
|
reasonCode: error instanceof Error ? error.message : 'HOLOLAKE_RELEASE_UNKNOWN_FAILURE',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonResponse(response, status, value) {
|
||||||
|
const body = Buffer.from(`${JSON.stringify(value)}\n`)
|
||||||
|
response.writeHead(status, {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Content-Length': body.length,
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
|
response.end(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createReleaseServer(runtimeState) {
|
||||||
|
return http.createServer((request, response) => {
|
||||||
|
response.setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
if (!['GET', 'HEAD'].includes(request.method || '')) {
|
||||||
|
response.writeHead(405, { Allow: 'GET, HEAD', 'Cache-Control': 'no-store' })
|
||||||
|
return response.end()
|
||||||
|
}
|
||||||
|
const pathname = new URL(request.url || '/', 'http://127.0.0.1').pathname
|
||||||
|
if (pathname === '/health') {
|
||||||
|
const status = runtimeState.state === 'LOCKED_INVALID_RELEASE_EVIDENCE' ? 503 : 200
|
||||||
|
return jsonResponse(response, status, {
|
||||||
|
schema: 'hololake.release-broadcast-health/v1',
|
||||||
|
state: runtimeState.state,
|
||||||
|
updateAvailable: runtimeState.release !== null,
|
||||||
|
automaticUpload: false,
|
||||||
|
automaticActivation: false,
|
||||||
|
upstreamUpdateSources: [],
|
||||||
|
...(runtimeState.release ? { releaseId: runtimeState.release.releaseId, version: runtimeState.release.version } : {}),
|
||||||
|
...(runtimeState.reasonCode ? { reasonCode: runtimeState.reasonCode } : {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (pathname === '/latest.json') {
|
||||||
|
if (runtimeState.state === 'EMPTY_FAIL_CLOSED') {
|
||||||
|
response.writeHead(204, { 'Cache-Control': 'no-store' })
|
||||||
|
return response.end()
|
||||||
|
}
|
||||||
|
if (!runtimeState.release) {
|
||||||
|
response.writeHead(503, { 'Cache-Control': 'no-store' })
|
||||||
|
return response.end()
|
||||||
|
}
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
'Content-Length': runtimeState.release.broadcastBytes.length,
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
|
return response.end(request.method === 'HEAD' ? undefined : runtimeState.release.broadcastBytes)
|
||||||
|
}
|
||||||
|
const packageFile = runtimeState.release?.packages.get(pathname)
|
||||||
|
if (packageFile) {
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': packageFile.size,
|
||||||
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
})
|
||||||
|
if (request.method === 'HEAD') return response.end()
|
||||||
|
return fs.createReadStream(packageFile.file).on('error', () => response.destroy()).pipe(response)
|
||||||
|
}
|
||||||
|
response.writeHead(404, { 'Cache-Control': 'no-store' })
|
||||||
|
response.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main(env = process.env) {
|
||||||
|
const host = env.HOLOLAKE_RELEASE_HOST || '127.0.0.1'
|
||||||
|
if (host !== '127.0.0.1') fail('HOLOLAKE_RELEASE_LOOPBACK_BIND_REQUIRED')
|
||||||
|
const port = Number(env.HOLOLAKE_RELEASE_PORT || 3940)
|
||||||
|
if (!Number.isInteger(port) || port < 1024 || port > 65535) fail('HOLOLAKE_RELEASE_PORT_INVALID')
|
||||||
|
const stateRoot = path.resolve(env.HOLOLAKE_RELEASE_STATE_ROOT || '/var/lib/guanghu/hololake-release-broadcast')
|
||||||
|
const runtimeState = loadRuntimeState(stateRoot, { enforceRootOwner: env.HOLOLAKE_RELEASE_ENFORCE_ROOT_OWNER === '1' })
|
||||||
|
const server = createReleaseServer(runtimeState)
|
||||||
|
server.on('clientError', (_error, socket) => socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'))
|
||||||
|
server.listen(port, host, () => {
|
||||||
|
process.stdout.write(`${JSON.stringify({ schema: 'hololake.release-broadcast-startup/v1', host, port, state: runtimeState.state })}\n`)
|
||||||
|
})
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
try {
|
||||||
|
main()
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||||
|
process.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue