20260826 两宿主全线正本/回执/状态 + 协议正本地图v1.2 + 模板/镜面/坑清单(元审计收敛commit;前人未提交改动未碰)
This commit is contained in:
parent
b95fcb57ba
commit
59fc9bfd7b
670 changed files with 133422 additions and 129 deletions
|
|
@ -0,0 +1,59 @@
|
|||
# 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`. The public HTTPS path is carried by a
|
||||
dedicated, port-forwarding-only identity from JD-FD-PRIMARY to BS-GZ-006 and
|
||||
currently returns the origin's empty fail-closed `204` response. Artifact
|
||||
upload, activation, and desktop rollout remain independent deployment gates.
|
||||
|
||||
The registered public namespace is `/hololake/releases`. The loopback origin
|
||||
accepts the manifest at both its operator health-check path `/latest.json` and
|
||||
the public updater path `/hololake/releases/latest.json`; release package URLs
|
||||
must remain under that same public prefix. The front-door proxy must therefore
|
||||
preserve the full request URI. The registered BS-GZ-006 loopback tunnel port is
|
||||
`19440`; it is not a public listener.
|
||||
|
||||
`render-front-door.mjs --loopback-port PORT` renders the bounded Nginx
|
||||
location. It refuses an omitted
|
||||
port, preserves the full public request path by leaving `proxy_pass` without a
|
||||
URI suffix, accepts only GET and HEAD, forwards no request body, and strips
|
||||
authorization, cookies, and client forwarding headers. Rendering is not
|
||||
deployment; Nginx validation, transactional install, public certificate/readback,
|
||||
and a separate server receipt remain required.
|
||||
|
||||
`operator.mjs` supplies the separate, root-operated verification and activation
|
||||
boundary. `verify` reconstructs a private candidate tree and accepts it only when
|
||||
the broadcast, pipeline receipt, package bytes, Developer ID receipt, Apple
|
||||
notarization receipt, and exact human approval all agree. `activate` additionally
|
||||
requires the operator to repeat the expected release id, version, source commit,
|
||||
and broadcast SHA-256. It copies only referenced immutable artifacts, commits
|
||||
`ACTIVE.json` atomically, and reports that an explicit service restart is still
|
||||
required. It never uploads, activates, or restarts on its own.
|
||||
|
||||
```text
|
||||
node operator.mjs verify \
|
||||
--source /secure/release/out/0.2.0 \
|
||||
--human-approval /secure/approvals/0.2.0.json
|
||||
|
||||
sudo node operator.mjs activate \
|
||||
--source /secure/release/out/0.2.0 \
|
||||
--human-approval /secure/approvals/0.2.0.json \
|
||||
--state-root /var/lib/guanghu/hololake-release-broadcast \
|
||||
--expect-release-id GH-HOLOLAKE-RELEASE-0.2.0 \
|
||||
--expect-version 0.2.0 \
|
||||
--expect-source-commit 0000000000000000000000000000000000000000 \
|
||||
--expect-broadcast-sha256 0000000000000000000000000000000000000000000000000000000000000000
|
||||
```
|
||||
|
||||
The zeros above are placeholders, not deployable values. A real activation must
|
||||
use the exact facts printed by `verify` and a separately issued human approval.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
[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
|
||||
Environment=HOLOLAKE_RELEASE_PUBLIC_PREFIX=/hololake/releases
|
||||
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,431 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { DEFAULT_PUBLIC_PREFIX, isMainModule, loadRuntimeState, normalizePublicPrefix } from './server.mjs'
|
||||
|
||||
const MAX_CONTROL_BYTES = 1024 * 1024
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
||||
const SOURCE_COMMIT_PATTERN = /^[a-f0-9]{40}$/
|
||||
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
||||
|
||||
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 requireTrustedDirectory(directory, { enforceRootOwner = false } = {}) {
|
||||
const info = fs.lstatSync(directory)
|
||||
if (!info.isDirectory() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_INVALID')
|
||||
if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_WRITABLE_BY_NON_OWNER')
|
||||
if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_NOT_ROOT_OWNED')
|
||||
return info
|
||||
}
|
||||
|
||||
function requireTrustedFile(file, { enforceRootOwner = false, maxBytes = null } = {}) {
|
||||
const info = fs.lstatSync(file)
|
||||
if (!info.isFile() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_INVALID')
|
||||
if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_WRITABLE_BY_NON_OWNER')
|
||||
if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_NOT_ROOT_OWNED')
|
||||
if (maxBytes !== null && info.size > maxBytes) fail('HOLOLAKE_RELEASE_OPERATOR_CONTROL_FILE_TOO_LARGE')
|
||||
return info
|
||||
}
|
||||
|
||||
function readJson(file, { enforceRootOwner = false } = {}) {
|
||||
requireTrustedFile(file, { enforceRootOwner, maxBytes: MAX_CONTROL_BYTES })
|
||||
const bytes = fs.readFileSync(file)
|
||||
return { bytes, value: JSON.parse(bytes.toString('utf8')) }
|
||||
}
|
||||
|
||||
function directFile(directory, name, code) {
|
||||
const value = requireText(name, code)
|
||||
if (value !== path.basename(value) || value === '.' || value === '..' || value.includes('\0')) fail(code)
|
||||
const resolved = path.resolve(directory, value)
|
||||
if (path.dirname(resolved) !== directory) fail(code)
|
||||
return resolved
|
||||
}
|
||||
|
||||
function packageNameFromUrl(value, publicPrefix) {
|
||||
const url = new URL(requireText(value, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_REQUIRED'))
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID')
|
||||
}
|
||||
let packageName
|
||||
try {
|
||||
packageName = decodeURIComponent(path.posix.basename(url.pathname))
|
||||
} catch {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID')
|
||||
}
|
||||
if (!packageName || packageName === '.' || packageName === '..') fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_NAME_INVALID')
|
||||
if (!url.pathname.startsWith(`${publicPrefix}/`) || url.pathname === `${publicPrefix}/latest.json` || url.pathname === `${publicPrefix}/health`) {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PUBLIC_PREFIX_MISMATCH')
|
||||
}
|
||||
return packageName
|
||||
}
|
||||
|
||||
function addArtifact(artifacts, source, name, type) {
|
||||
const previous = artifacts.get(name)
|
||||
if (previous && previous.source !== source) fail('HOLOLAKE_RELEASE_OPERATOR_ARTIFACT_NAME_COLLISION')
|
||||
artifacts.set(name, { source, name, type })
|
||||
}
|
||||
|
||||
export function inspectReleaseBundle(
|
||||
sourceDirectory,
|
||||
humanApprovalPath,
|
||||
{ enforceRootOwner = false, publicPrefix = DEFAULT_PUBLIC_PREFIX } = {},
|
||||
) {
|
||||
const source = path.resolve(sourceDirectory)
|
||||
const normalizedPublicPrefix = normalizePublicPrefix(publicPrefix)
|
||||
requireTrustedDirectory(source, { enforceRootOwner })
|
||||
const latestPath = directFile(source, 'latest.json', 'HOLOLAKE_RELEASE_OPERATOR_BROADCAST_PATH_INVALID')
|
||||
const pipelinePath = directFile(source, 'pipeline-receipt.json', 'HOLOLAKE_RELEASE_OPERATOR_PIPELINE_PATH_INVALID')
|
||||
const approvalPath = path.resolve(humanApprovalPath)
|
||||
const latestDocument = readJson(latestPath, { enforceRootOwner })
|
||||
const pipelineReceipt = readJson(pipelinePath, { enforceRootOwner }).value
|
||||
const approvalReceipt = readJson(approvalPath, { enforceRootOwner }).value
|
||||
const broadcast = latestDocument.value
|
||||
const broadcastSha256 = sha256(latestDocument.bytes)
|
||||
|
||||
if (broadcast?.schema !== 'hololake.release-broadcast/v1') fail('HOLOLAKE_RELEASE_OPERATOR_BROADCAST_INVALID')
|
||||
const releaseId = requireText(broadcast.releaseId, 'HOLOLAKE_RELEASE_OPERATOR_RELEASE_ID_REQUIRED')
|
||||
const version = requireText(broadcast.version, 'HOLOLAKE_RELEASE_OPERATOR_VERSION_REQUIRED')
|
||||
if (!VERSION_PATTERN.test(version)) fail('HOLOLAKE_RELEASE_OPERATOR_VERSION_INVALID')
|
||||
if (
|
||||
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_OPERATOR_BROADCAST_POLICY_INVALID')
|
||||
|
||||
if (
|
||||
pipelineReceipt?.schema !== 'hololake.signed-release-pipeline-receipt/v1' ||
|
||||
pipelineReceipt?.state !== 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD' ||
|
||||
pipelineReceipt?.broadcastSha256 !== broadcastSha256 ||
|
||||
!SOURCE_COMMIT_PATTERN.test(pipelineReceipt?.sourceCommit || '') ||
|
||||
pipelineReceipt?.automaticUpload !== false ||
|
||||
pipelineReceipt?.automaticActivation !== false
|
||||
) fail('HOLOLAKE_RELEASE_OPERATOR_PIPELINE_RECEIPT_INVALID')
|
||||
|
||||
const approvalId = requireText(approvalReceipt?.approvalId, 'HOLOLAKE_RELEASE_OPERATOR_APPROVAL_ID_REQUIRED')
|
||||
if (
|
||||
approvalReceipt?.schema !== 'hololake.release-broadcast-human-approval/v1' ||
|
||||
approvalReceipt?.state !== 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE' ||
|
||||
approvalReceipt?.releaseId !== releaseId ||
|
||||
approvalReceipt?.version !== version ||
|
||||
approvalReceipt?.broadcastSha256 !== broadcastSha256
|
||||
) fail('HOLOLAKE_RELEASE_OPERATOR_HUMAN_APPROVAL_INVALID')
|
||||
|
||||
const artifacts = new Map()
|
||||
addArtifact(artifacts, latestPath, 'latest.json', 'control')
|
||||
addArtifact(artifacts, pipelinePath, 'pipeline-receipt.json', 'control')
|
||||
addArtifact(artifacts, approvalPath, 'human-approval.json', 'control')
|
||||
for (const platform of Object.values(broadcast.platforms)) {
|
||||
const packageName = packageNameFromUrl(platform?.url, normalizedPublicPrefix)
|
||||
const packagePath = directFile(source, packageName, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PATH_INVALID')
|
||||
const packageInfo = requireTrustedFile(packagePath, { enforceRootOwner })
|
||||
if (!Number.isSafeInteger(platform.size) || platform.size <= 0 || platform.size !== packageInfo.size) {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SIZE_MISMATCH')
|
||||
}
|
||||
if (!SHA256_PATTERN.test(platform.sha256 || '') || sha256(fs.readFileSync(packagePath)) !== platform.sha256) {
|
||||
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SHA256_MISMATCH')
|
||||
}
|
||||
requireText(platform.signature, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SIGNATURE_REQUIRED')
|
||||
const codeReceiptId = requireText(platform.platformCodeSignatureReceipt, 'HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_REQUIRED')
|
||||
const notarizationReceiptId = requireText(platform.notarizationReceipt, 'HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_REQUIRED')
|
||||
const codeReceiptName = `${codeReceiptId}.json`
|
||||
const notarizationReceiptName = `${notarizationReceiptId}.json`
|
||||
const codeReceiptPath = directFile(source, codeReceiptName, 'HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_PATH_INVALID')
|
||||
const notarizationReceiptPath = directFile(source, notarizationReceiptName, 'HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_PATH_INVALID')
|
||||
const codeReceipt = readJson(codeReceiptPath, { enforceRootOwner }).value
|
||||
const notarizationReceipt = readJson(notarizationReceiptPath, { enforceRootOwner }).value
|
||||
if (
|
||||
codeReceipt?.schema !== 'hololake.platform-code-signature-receipt/v1' ||
|
||||
codeReceipt?.state !== 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED' ||
|
||||
codeReceipt?.sourceCommit !== pipelineReceipt.sourceCommit
|
||||
) fail('HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_INVALID')
|
||||
if (
|
||||
notarizationReceipt?.schema !== 'hololake.apple-notarization-receipt/v1' ||
|
||||
notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED' ||
|
||||
notarizationReceipt?.sourceCommit !== pipelineReceipt.sourceCommit
|
||||
) fail('HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_INVALID')
|
||||
addArtifact(artifacts, packagePath, packageName, 'package')
|
||||
addArtifact(artifacts, codeReceiptPath, codeReceiptName, 'control')
|
||||
addArtifact(artifacts, notarizationReceiptPath, notarizationReceiptName, 'control')
|
||||
}
|
||||
|
||||
return {
|
||||
schema: 'hololake.release-broadcast-operator-plan/v1',
|
||||
state: 'EXACT_SIGNED_NOTARIZED_HUMAN_APPROVED_BUNDLE_VERIFIED',
|
||||
releaseId,
|
||||
version,
|
||||
sourceCommit: pipelineReceipt.sourceCommit,
|
||||
broadcastSha256,
|
||||
approvalId,
|
||||
publicPrefix: normalizedPublicPrefix,
|
||||
artifacts: [...artifacts.values()],
|
||||
}
|
||||
}
|
||||
|
||||
function copyFileNoFollow(source, destination, mode) {
|
||||
const sourceDescriptor = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
|
||||
let destinationDescriptor = null
|
||||
try {
|
||||
const sourceInfo = fs.fstatSync(sourceDescriptor)
|
||||
if (!sourceInfo.isFile() || (sourceInfo.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_SOURCE_CHANGED')
|
||||
destinationDescriptor = fs.openSync(destination, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode)
|
||||
const buffer = Buffer.allocUnsafe(1024 * 1024)
|
||||
let position = 0
|
||||
while (position < sourceInfo.size) {
|
||||
const read = fs.readSync(sourceDescriptor, buffer, 0, Math.min(buffer.length, sourceInfo.size - position), position)
|
||||
if (read <= 0) fail('HOLOLAKE_RELEASE_OPERATOR_SOURCE_TRUNCATED')
|
||||
let written = 0
|
||||
while (written < read) written += fs.writeSync(destinationDescriptor, buffer, written, read - written)
|
||||
position += read
|
||||
}
|
||||
fs.fsyncSync(destinationDescriptor)
|
||||
} finally {
|
||||
if (destinationDescriptor !== null) fs.closeSync(destinationDescriptor)
|
||||
fs.closeSync(sourceDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function writeJsonExclusive(file, value, mode) {
|
||||
const descriptor = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode)
|
||||
try {
|
||||
fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`)
|
||||
fs.fsyncSync(descriptor)
|
||||
} finally {
|
||||
fs.closeSync(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
function activationFor(plan) {
|
||||
return {
|
||||
schema: 'hololake.release-broadcast-activation/v1',
|
||||
state: 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE',
|
||||
releaseId: plan.releaseId,
|
||||
version: plan.version,
|
||||
broadcastRelativePath: `releases/${plan.version}/latest.json`,
|
||||
broadcastSha256: plan.broadcastSha256,
|
||||
pipelineReceiptRelativePath: `releases/${plan.version}/pipeline-receipt.json`,
|
||||
humanApprovalReceipt: plan.approvalId,
|
||||
humanApprovalReceiptRelativePath: `releases/${plan.version}/human-approval.json`,
|
||||
}
|
||||
}
|
||||
|
||||
function prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId }) {
|
||||
const releases = path.join(candidateRoot, 'releases')
|
||||
const releaseDirectory = path.join(releases, plan.version)
|
||||
fs.mkdirSync(releaseDirectory, { recursive: true, mode: enforceRootOwner ? 0o750 : 0o700 })
|
||||
for (const artifact of plan.artifacts) {
|
||||
const destination = path.join(releaseDirectory, artifact.name)
|
||||
copyFileNoFollow(artifact.source, destination, enforceRootOwner ? 0o640 : 0o600)
|
||||
if (enforceRootOwner) fs.chownSync(destination, 0, trustedGroupId)
|
||||
}
|
||||
const activationPath = path.join(candidateRoot, 'ACTIVE.json')
|
||||
writeJsonExclusive(activationPath, activationFor(plan), enforceRootOwner ? 0o640 : 0o600)
|
||||
if (enforceRootOwner) {
|
||||
for (const directory of [candidateRoot, releases, releaseDirectory]) {
|
||||
fs.chownSync(directory, 0, trustedGroupId)
|
||||
fs.chmodSync(directory, 0o750)
|
||||
}
|
||||
fs.chownSync(activationPath, 0, trustedGroupId)
|
||||
}
|
||||
const state = loadRuntimeState(candidateRoot, { enforceRootOwner, publicPrefix: plan.publicPrefix })
|
||||
if (state.state !== 'READY_SIGNED_NOTARIZED_BROADCAST') {
|
||||
fail(`HOLOLAKE_RELEASE_OPERATOR_CANDIDATE_INVALID:${state.reasonCode || state.state}`)
|
||||
}
|
||||
return { releaseDirectory, activationPath }
|
||||
}
|
||||
|
||||
function requireExpected(plan, expected) {
|
||||
for (const [field, code] of [
|
||||
['releaseId', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_RELEASE_ID_MISMATCH'],
|
||||
['version', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_VERSION_MISMATCH'],
|
||||
['sourceCommit', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_SOURCE_COMMIT_MISMATCH'],
|
||||
['broadcastSha256', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_BROADCAST_SHA256_MISMATCH'],
|
||||
]) {
|
||||
if (requireText(expected?.[field], `${code}_REQUIRED`) !== plan[field]) fail(code)
|
||||
}
|
||||
}
|
||||
|
||||
function replaceActivation(stateRoot, candidateActivation, previousBytes, enforceRootOwner, trustedGroupId, publicPrefix) {
|
||||
const tempActivation = path.join(stateRoot, `.ACTIVE.${crypto.randomUUID()}.tmp`)
|
||||
copyFileNoFollow(candidateActivation, tempActivation, enforceRootOwner ? 0o640 : 0o600)
|
||||
if (enforceRootOwner) fs.chownSync(tempActivation, 0, trustedGroupId)
|
||||
fs.renameSync(tempActivation, path.join(stateRoot, 'ACTIVE.json'))
|
||||
const committed = loadRuntimeState(stateRoot, { enforceRootOwner, publicPrefix })
|
||||
if (committed.state === 'READY_SIGNED_NOTARIZED_BROADCAST') return committed
|
||||
const failedActive = path.join(stateRoot, 'ACTIVE.json')
|
||||
if (previousBytes === null) {
|
||||
fs.unlinkSync(failedActive)
|
||||
} else {
|
||||
const rollback = path.join(stateRoot, `.ACTIVE.rollback.${crypto.randomUUID()}.tmp`)
|
||||
const descriptor = fs.openSync(rollback, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, enforceRootOwner ? 0o640 : 0o600)
|
||||
try {
|
||||
fs.writeFileSync(descriptor, previousBytes)
|
||||
fs.fsyncSync(descriptor)
|
||||
} finally {
|
||||
fs.closeSync(descriptor)
|
||||
}
|
||||
if (enforceRootOwner) fs.chownSync(rollback, 0, trustedGroupId)
|
||||
fs.renameSync(rollback, failedActive)
|
||||
}
|
||||
fail(`HOLOLAKE_RELEASE_OPERATOR_POST_COMMIT_INVALID:${committed.reasonCode || committed.state}`)
|
||||
}
|
||||
|
||||
function acquireOperatorLock(stateRoot, enforceRootOwner, trustedGroupId) {
|
||||
const lockDirectory = path.join(stateRoot, '.operator-lock')
|
||||
try {
|
||||
fs.mkdirSync(lockDirectory, { mode: enforceRootOwner ? 0o750 : 0o700 })
|
||||
} catch (error) {
|
||||
if (error?.code === 'EEXIST') fail('HOLOLAKE_RELEASE_OPERATOR_LOCKED')
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
if (enforceRootOwner) fs.chownSync(lockDirectory, 0, trustedGroupId)
|
||||
writeJsonExclusive(path.join(lockDirectory, 'owner.json'), {
|
||||
schema: 'hololake.release-broadcast-operator-lock/v1',
|
||||
pid: process.pid,
|
||||
acquiredAt: new Date().toISOString(),
|
||||
}, enforceRootOwner ? 0o640 : 0o600)
|
||||
if (enforceRootOwner) fs.chownSync(path.join(lockDirectory, 'owner.json'), 0, trustedGroupId)
|
||||
return lockDirectory
|
||||
} catch (error) {
|
||||
fs.rmSync(lockDirectory, { recursive: true, force: true })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyReleaseBundle(sourceDirectory, humanApprovalPath) {
|
||||
const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath)
|
||||
const candidateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-operator-verify-'))
|
||||
try {
|
||||
prepareCandidate(candidateRoot, plan, { enforceRootOwner: false, trustedGroupId: process.getgid?.() ?? 0 })
|
||||
return { ...plan, artifacts: plan.artifacts.map(({ name, type }) => ({ name, type })) }
|
||||
} finally {
|
||||
fs.rmSync(candidateRoot, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
export function activateRelease({
|
||||
sourceDirectory,
|
||||
humanApprovalPath,
|
||||
stateRoot,
|
||||
expected,
|
||||
enforceRootOwner = true,
|
||||
publicPrefix = DEFAULT_PUBLIC_PREFIX,
|
||||
}) {
|
||||
if (enforceRootOwner && process.getuid?.() !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_ROOT_REQUIRED')
|
||||
const root = path.resolve(stateRoot)
|
||||
const rootInfo = requireTrustedDirectory(root, { enforceRootOwner })
|
||||
const trustedGroupId = rootInfo.gid
|
||||
const lockDirectory = acquireOperatorLock(root, enforceRootOwner, trustedGroupId)
|
||||
let candidateRoot = null
|
||||
try {
|
||||
const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner, publicPrefix })
|
||||
requireExpected(plan, expected)
|
||||
candidateRoot = fs.mkdtempSync(path.join(root, '.candidate-'))
|
||||
const candidate = prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId })
|
||||
const releasesRoot = path.join(root, 'releases')
|
||||
if (!fs.existsSync(releasesRoot)) fs.mkdirSync(releasesRoot, { mode: enforceRootOwner ? 0o750 : 0o700 })
|
||||
requireTrustedDirectory(releasesRoot, { enforceRootOwner })
|
||||
if (enforceRootOwner) {
|
||||
fs.chownSync(releasesRoot, 0, trustedGroupId)
|
||||
fs.chmodSync(releasesRoot, 0o750)
|
||||
}
|
||||
const finalRelease = path.join(releasesRoot, plan.version)
|
||||
if (fs.existsSync(finalRelease)) fail('HOLOLAKE_RELEASE_OPERATOR_VERSION_ALREADY_STAGED')
|
||||
const activePath = path.join(root, 'ACTIVE.json')
|
||||
const previousBytes = fs.existsSync(activePath) ? readJson(activePath, { enforceRootOwner }).bytes : null
|
||||
fs.renameSync(candidate.releaseDirectory, finalRelease)
|
||||
let committed
|
||||
try {
|
||||
committed = replaceActivation(
|
||||
root,
|
||||
candidate.activationPath,
|
||||
previousBytes,
|
||||
enforceRootOwner,
|
||||
trustedGroupId,
|
||||
plan.publicPrefix,
|
||||
)
|
||||
} catch (error) {
|
||||
fs.rmSync(finalRelease, { recursive: true, force: true })
|
||||
throw error
|
||||
}
|
||||
return {
|
||||
schema: 'hololake.release-broadcast-operator-receipt/v1',
|
||||
state: 'ACTIVATED_EXPLICIT_SERVICE_RESTART_REQUIRED',
|
||||
releaseId: plan.releaseId,
|
||||
version: plan.version,
|
||||
sourceCommit: plan.sourceCommit,
|
||||
broadcastSha256: plan.broadcastSha256,
|
||||
approvalId: plan.approvalId,
|
||||
publicPrefix: plan.publicPrefix,
|
||||
automaticUpload: false,
|
||||
automaticActivation: false,
|
||||
automaticRestart: false,
|
||||
runtimeStateAfterDiskCommit: committed.state,
|
||||
}
|
||||
} finally {
|
||||
if (candidateRoot) fs.rmSync(candidateRoot, { recursive: true, force: true })
|
||||
fs.rmSync(lockDirectory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const command = argv[0]
|
||||
const values = {}
|
||||
for (let index = 1; index < argv.length; index += 1) {
|
||||
const key = argv[index]
|
||||
if (!key.startsWith('--') || index + 1 >= argv.length) fail('HOLOLAKE_RELEASE_OPERATOR_ARGUMENT_INVALID')
|
||||
values[key.slice(2)] = argv[++index]
|
||||
}
|
||||
return { command, values }
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2)) {
|
||||
const { command, values } = parseArguments(argv)
|
||||
const sourceDirectory = requireText(values.source, 'HOLOLAKE_RELEASE_OPERATOR_SOURCE_REQUIRED')
|
||||
const humanApprovalPath = requireText(values['human-approval'], 'HOLOLAKE_RELEASE_OPERATOR_HUMAN_APPROVAL_PATH_REQUIRED')
|
||||
if (command === 'verify') {
|
||||
process.stdout.write(`${JSON.stringify(verifyReleaseBundle(sourceDirectory, humanApprovalPath), null, 2)}\n`)
|
||||
return
|
||||
}
|
||||
if (command !== 'activate') fail('HOLOLAKE_RELEASE_OPERATOR_COMMAND_INVALID:verify|activate')
|
||||
const receipt = activateRelease({
|
||||
sourceDirectory,
|
||||
humanApprovalPath,
|
||||
stateRoot: requireText(values['state-root'], 'HOLOLAKE_RELEASE_OPERATOR_STATE_ROOT_REQUIRED'),
|
||||
publicPrefix: values['public-prefix'] || DEFAULT_PUBLIC_PREFIX,
|
||||
expected: {
|
||||
releaseId: values['expect-release-id'],
|
||||
version: values['expect-version'],
|
||||
sourceCommit: values['expect-source-commit'],
|
||||
broadcastSha256: values['expect-broadcast-sha256'],
|
||||
},
|
||||
})
|
||||
process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`)
|
||||
}
|
||||
|
||||
if (isMainModule(process.argv[1], import.meta.url)) {
|
||||
try {
|
||||
main()
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"schema": "hololake.release-public-route/v1",
|
||||
"state": "DEPLOYED_EMPTY_FAIL_CLOSED",
|
||||
"publicBaseUrl": "https://guanghulab.com/hololake/releases",
|
||||
"releaseEndpoint": "https://guanghulab.com/hololake/releases/latest.json",
|
||||
"publicPathPrefix": "/hololake/releases",
|
||||
"originNode": "JD-FD-PRIMARY",
|
||||
"originListener": "127.0.0.1:3940",
|
||||
"frontDoorNode": "BS-GZ-006",
|
||||
"frontDoorLoopbackPort": 19440,
|
||||
"proxyRequestUriPolicy": "PRESERVE_FULL_PUBLIC_PATH",
|
||||
"allowedMethods": ["GET", "HEAD"],
|
||||
"requestBodyAllowed": false,
|
||||
"latestCachePolicy": "NO_STORE",
|
||||
"packageCachePolicy": "PUBLIC_IMMUTABLE",
|
||||
"automaticUpload": false,
|
||||
"automaticActivation": false,
|
||||
"deployed": true
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const PORT_PATTERN = /^(?:[1-9]\d{3,4})$/
|
||||
|
||||
const fail = (code) => {
|
||||
throw new Error(code)
|
||||
}
|
||||
|
||||
export function renderFrontDoorSnippet(portValue) {
|
||||
const text = String(portValue ?? '')
|
||||
if (!PORT_PATTERN.test(text)) fail('HOLOLAKE_RELEASE_FRONT_DOOR_PORT_REQUIRED')
|
||||
const port = Number(text)
|
||||
if (!Number.isInteger(port) || port < 1024 || port > 65535) fail('HOLOLAKE_RELEASE_FRONT_DOOR_PORT_INVALID')
|
||||
return `location = /hololake/releases {
|
||||
return 308 /hololake/releases/;
|
||||
}
|
||||
|
||||
location ^~ /hololake/releases/ {
|
||||
limit_except GET HEAD { deny all; }
|
||||
client_max_body_size 0;
|
||||
proxy_pass http://127.0.0.1:${port};
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Authorization "";
|
||||
proxy_set_header Cookie "";
|
||||
proxy_set_header X-Forwarded-For "";
|
||||
proxy_set_header X-Forwarded-Host "";
|
||||
proxy_set_header X-Forwarded-Proto "";
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_connect_timeout 2s;
|
||||
proxy_send_timeout 10s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2)) {
|
||||
const portIndex = argv.indexOf('--loopback-port')
|
||||
if (portIndex < 0 || portIndex + 1 >= argv.length || argv.length !== 2) {
|
||||
fail('HOLOLAKE_RELEASE_FRONT_DOOR_USAGE:--loopback-port PORT')
|
||||
}
|
||||
process.stdout.write(renderFrontDoorSnippet(argv[portIndex + 1]))
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
||||
try {
|
||||
main()
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
#!/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
|
||||
export const DEFAULT_PUBLIC_PREFIX = '/hololake/releases'
|
||||
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()
|
||||
}
|
||||
|
||||
export function normalizePublicPrefix(value) {
|
||||
const prefix = requireText(value, 'HOLOLAKE_RELEASE_PUBLIC_PREFIX_REQUIRED')
|
||||
if (!prefix.startsWith('/') || prefix.endsWith('/') || prefix.includes('//') || prefix.includes('?') || prefix.includes('#')) {
|
||||
fail('HOLOLAKE_RELEASE_PUBLIC_PREFIX_INVALID')
|
||||
}
|
||||
const normalized = path.posix.normalize(prefix)
|
||||
if (normalized !== prefix || normalized === '/' || normalized.split('/').some((segment) => segment === '..')) {
|
||||
fail('HOLOLAKE_RELEASE_PUBLIC_PREFIX_INVALID')
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
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, sourceCommit, publicPrefix) {
|
||||
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')
|
||||
if (!url.pathname.startsWith(`${publicPrefix}/`) || url.pathname === `${publicPrefix}/latest.json` || url.pathname === `${publicPrefix}/health`) {
|
||||
fail('HOLOLAKE_RELEASE_PACKAGE_PUBLIC_PREFIX_MISMATCH')
|
||||
}
|
||||
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' ||
|
||||
codeReceipt?.sourceCommit !== sourceCommit
|
||||
) fail('HOLOLAKE_RELEASE_CODESIGN_RECEIPT_INVALID')
|
||||
if (
|
||||
notarizationReceipt?.schema !== 'hololake.apple-notarization-receipt/v1' ||
|
||||
notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED' ||
|
||||
notarizationReceipt?.sourceCommit !== sourceCommit
|
||||
) fail('HOLOLAKE_RELEASE_NOTARIZATION_RECEIPT_INVALID')
|
||||
return { pathname: url.pathname, file: packagePath, size: packageInfo.size }
|
||||
}
|
||||
|
||||
function loadActiveRelease(stateRoot, enforceRootOwner, publicPrefix) {
|
||||
const activationPath = path.join(stateRoot, 'ACTIVE.json')
|
||||
if (!fs.existsSync(activationPath)) {
|
||||
return { state: 'EMPTY_FAIL_CLOSED', release: null, reasonCode: null, publicPrefix }
|
||||
}
|
||||
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 approvalPath = safeRelative(stateRoot, activation.humanApprovalReceiptRelativePath, 'HOLOLAKE_RELEASE_HUMAN_APPROVAL_PATH_INVALID')
|
||||
const broadcastDocument = readTrustedJson(broadcastPath, { enforceRootOwner })
|
||||
const pipelineReceipt = readTrustedJson(pipelinePath, { enforceRootOwner }).value
|
||||
const approvalReceipt = readTrustedJson(approvalPath, { 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 ||
|
||||
!/^[a-f0-9]{40}$/.test(pipelineReceipt?.sourceCommit || '') ||
|
||||
pipelineReceipt?.automaticUpload !== false ||
|
||||
pipelineReceipt?.automaticActivation !== false
|
||||
) fail('HOLOLAKE_RELEASE_PIPELINE_RECEIPT_INVALID')
|
||||
if (
|
||||
approvalReceipt?.schema !== 'hololake.release-broadcast-human-approval/v1' ||
|
||||
approvalReceipt?.state !== 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE' ||
|
||||
approvalReceipt?.approvalId !== activation.humanApprovalReceipt ||
|
||||
approvalReceipt?.releaseId !== releaseId ||
|
||||
approvalReceipt?.version !== version ||
|
||||
approvalReceipt?.broadcastSha256 !== activation.broadcastSha256
|
||||
) fail('HOLOLAKE_RELEASE_HUMAN_APPROVAL_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, pipelineReceipt.sourceCommit, publicPrefix)
|
||||
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,
|
||||
publicPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
export function loadRuntimeState(stateRoot, { enforceRootOwner = false, publicPrefix = DEFAULT_PUBLIC_PREFIX } = {}) {
|
||||
const resolvedRoot = path.resolve(stateRoot)
|
||||
try {
|
||||
const normalizedPrefix = normalizePublicPrefix(publicPrefix)
|
||||
return loadActiveRelease(resolvedRoot, enforceRootOwner, normalizedPrefix)
|
||||
} catch (error) {
|
||||
return {
|
||||
state: 'LOCKED_INVALID_RELEASE_EVIDENCE',
|
||||
release: null,
|
||||
reasonCode: error instanceof Error ? error.message : 'HOLOLAKE_RELEASE_UNKNOWN_FAILURE',
|
||||
publicPrefix: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
const publicHealthPath = runtimeState.publicPrefix ? `${runtimeState.publicPrefix}/health` : null
|
||||
const publicLatestPath = runtimeState.publicPrefix ? `${runtimeState.publicPrefix}/latest.json` : null
|
||||
if (pathname === '/health' || pathname === publicHealthPath) {
|
||||
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.publicPrefix ? {
|
||||
publicPathPrefix: runtimeState.publicPrefix,
|
||||
releaseEndpointPath: `${runtimeState.publicPrefix}/latest.json`,
|
||||
} : {}),
|
||||
...(runtimeState.release ? { releaseId: runtimeState.release.releaseId, version: runtimeState.release.version } : {}),
|
||||
...(runtimeState.reasonCode ? { reasonCode: runtimeState.reasonCode } : {}),
|
||||
})
|
||||
}
|
||||
if (pathname === '/latest.json' || pathname === publicLatestPath) {
|
||||
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 publicPrefix = env.HOLOLAKE_RELEASE_PUBLIC_PREFIX || DEFAULT_PUBLIC_PREFIX
|
||||
const runtimeState = loadRuntimeState(stateRoot, {
|
||||
enforceRootOwner: env.HOLOLAKE_RELEASE_ENFORCE_ROOT_OWNER === '1',
|
||||
publicPrefix,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
export function isMainModule(argvPath, moduleUrl) {
|
||||
return Boolean(argvPath) && fs.realpathSync(path.resolve(argvPath)) === fs.realpathSync(fileURLToPath(moduleUrl))
|
||||
}
|
||||
|
||||
if (isMainModule(process.argv[1], 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