feat(hololake): add mobile server account and capability proxy
This commit is contained in:
parent
758eef79ab
commit
19bd38c6e3
11 changed files with 1436 additions and 1 deletions
|
|
@ -10,6 +10,12 @@ const { sendSmtpMail } = require("./smtp-mailer");
|
|||
const { executeRegisteredAction } = require("./action-client");
|
||||
const { enqueueDeploymentEvent } = require("./deployment-event");
|
||||
const { GuanghuRouter, loadDevices } = require("./guanghu-router");
|
||||
const { HoloLakeSessionManager } = require("./hololake-session");
|
||||
const {
|
||||
HoloLakeAiGateway,
|
||||
HoloLakeKnowledgeProvider,
|
||||
loadAiProviders,
|
||||
} = require("./hololake-capabilities");
|
||||
const {
|
||||
loadRegistry: loadRepoPushRegistry,
|
||||
receiveBundle,
|
||||
|
|
@ -71,6 +77,72 @@ function createApp(options = {}) {
|
|||
smtpUser: process.env.SMTP_USER || ownerEmail,
|
||||
smtpPass: process.env.QQ_SMTP_AUTH_CODE || "",
|
||||
}));
|
||||
const hololakePepper = String(
|
||||
options.hololakeSessionPepper
|
||||
|| process.env.HOLOLAKE_SESSION_PEPPER
|
||||
|| "",
|
||||
);
|
||||
const hololakeSessionManager = options.hololakeSessionManager || (
|
||||
hololakePepper
|
||||
? new HoloLakeSessionManager({
|
||||
registeredEmails: approvers.map(item => item.email),
|
||||
pepper: hololakePepper,
|
||||
stateFile: Object.prototype.hasOwnProperty.call(options, "hololakeSessionStateFile")
|
||||
? options.hololakeSessionStateFile
|
||||
: (
|
||||
process.env.HOLOLAKE_SESSION_STATE_FILE
|
||||
|| "/var/lib/guanghu/lake-lamp-authz/hololake-sessions.json"
|
||||
),
|
||||
otpTtlSeconds: Number(
|
||||
options.hololakeOtpTtlSeconds
|
||||
|| process.env.HOLOLAKE_OTP_TTL
|
||||
|| 10 * 60,
|
||||
),
|
||||
sessionTtlSeconds: Number(
|
||||
options.hololakeSessionTtlSeconds
|
||||
|| process.env.HOLOLAKE_ACCOUNT_SESSION_TTL
|
||||
|| 24 * 60 * 60,
|
||||
),
|
||||
requestLimit: Number(
|
||||
options.hololakeOtpRequestLimit
|
||||
|| process.env.HOLOLAKE_OTP_REQUEST_LIMIT
|
||||
|| 6,
|
||||
),
|
||||
sendEmail,
|
||||
})
|
||||
: null
|
||||
);
|
||||
const hololakeKnowledgePath = String(
|
||||
options.hololakeKnowledgeRepositoryPath
|
||||
|| process.env.HOLOLAKE_KNOWLEDGE_REPOSITORY_PATH
|
||||
|| "",
|
||||
);
|
||||
const hololakeKnowledgeProvider = options.hololakeKnowledgeProvider || (
|
||||
hololakeKnowledgePath
|
||||
? new HoloLakeKnowledgeProvider({
|
||||
repositoryId: "bingshuo/hololake-knowledge-base",
|
||||
repositoryPath: hololakeKnowledgePath,
|
||||
maxArchiveBytes: Number(
|
||||
options.hololakeKnowledgeMaxArchiveBytes
|
||||
|| process.env.HOLOLAKE_KNOWLEDGE_MAX_ARCHIVE_BYTES
|
||||
|| 128 * 1024 * 1024,
|
||||
),
|
||||
})
|
||||
: null
|
||||
);
|
||||
const hololakeAiProvidersFile = String(
|
||||
options.hololakeAiProvidersFile
|
||||
|| process.env.HOLOLAKE_AI_PROVIDERS_FILE
|
||||
|| "",
|
||||
);
|
||||
const hololakeAiProviders = options.hololakeAiProviders || (
|
||||
hololakeAiProvidersFile ? loadAiProviders(hololakeAiProvidersFile) : {}
|
||||
);
|
||||
const hololakeAiGateway = options.hololakeAiGateway || (
|
||||
Object.keys(hololakeAiProviders).length > 0
|
||||
? new HoloLakeAiGateway({ providers: hololakeAiProviders })
|
||||
: null
|
||||
);
|
||||
const mapGate = options.mapGate || new MapGate({
|
||||
mapsDir: options.mapsDir || process.env.LAKE_LAMP_MAPS_DIR || "/etc/guanghu/navigation-maps",
|
||||
stateFile: Object.prototype.hasOwnProperty.call(options, "mapStateFile") ? options.mapStateFile : (process.env.LAKE_LAMP_MAP_STATE_FILE || "/var/lib/guanghu/lake-lamp-authz/map-acks.json"),
|
||||
|
|
@ -139,6 +211,23 @@ function createApp(options = {}) {
|
|||
return { ok: true, order: issued.order };
|
||||
}
|
||||
|
||||
function authenticateHoloLake(req) {
|
||||
if (!hololakeSessionManager) {
|
||||
return { ok: false, status: 503, error: "hololake_session_unavailable" };
|
||||
}
|
||||
const token = bearer(req);
|
||||
if (!token) return { ok: false, status: 401, error: "session_required" };
|
||||
const deviceId = String(req.headers["x-hololake-device-id"] || "");
|
||||
const authenticated = hololakeSessionManager.authenticate(token, deviceId);
|
||||
if (!authenticated.ok) {
|
||||
return {
|
||||
...authenticated,
|
||||
status: authenticated.error === "session_device_mismatch" ? 403 : 401,
|
||||
};
|
||||
}
|
||||
return { ...authenticated, token, deviceId };
|
||||
}
|
||||
|
||||
function bindAndDeliver(created) {
|
||||
const inspected = manager.inspectHandoff(created.handoffToken);
|
||||
if (!inspected.ok) return { delivered: 0, approver: null, order: null };
|
||||
|
|
@ -190,6 +279,206 @@ function createApp(options = {}) {
|
|||
},
|
||||
});
|
||||
|
||||
if (
|
||||
req.method === "POST"
|
||||
&& url.pathname === "/api/hololake/session/email/request"
|
||||
) {
|
||||
if (!hololakeSessionManager) {
|
||||
return json(res, 503, failure("hololake_session_unavailable"));
|
||||
}
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
const headerDeviceId = String(
|
||||
req.headers["x-hololake-device-id"] || "",
|
||||
);
|
||||
if (
|
||||
headerDeviceId
|
||||
&& body.device_id
|
||||
&& !safeEqual(headerDeviceId, String(body.device_id))
|
||||
) {
|
||||
return json(res, 400, failure("invalid_device"));
|
||||
}
|
||||
const requested = await hololakeSessionManager.requestOtp({
|
||||
email: body.email,
|
||||
deviceId: String(body.device_id || headerDeviceId),
|
||||
networkKey: clientAddress(req),
|
||||
});
|
||||
if (!requested.accepted) {
|
||||
return json(
|
||||
res,
|
||||
requested.error === "rate_limited" ? 429 : 400,
|
||||
failure(requested.error),
|
||||
);
|
||||
}
|
||||
return json(res, 202, {
|
||||
accepted: true,
|
||||
request_id: requested.request_id,
|
||||
expires_in: Number(
|
||||
options.hololakeOtpTtlSeconds
|
||||
|| process.env.HOLOLAKE_OTP_TTL
|
||||
|| 10 * 60,
|
||||
),
|
||||
next_step: "如果邮箱已登记,输入邮件中的六位验证码。",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
req.method === "POST"
|
||||
&& url.pathname === "/api/hololake/session/email/verify"
|
||||
) {
|
||||
if (!hololakeSessionManager) {
|
||||
return json(res, 503, failure("hololake_session_unavailable"));
|
||||
}
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
const headerDeviceId = String(
|
||||
req.headers["x-hololake-device-id"] || "",
|
||||
);
|
||||
if (
|
||||
headerDeviceId
|
||||
&& body.device_id
|
||||
&& !safeEqual(headerDeviceId, String(body.device_id))
|
||||
) {
|
||||
return json(res, 400, failure("invalid_device"));
|
||||
}
|
||||
const verified = hololakeSessionManager.verifyOtp({
|
||||
requestId: body.request_id,
|
||||
code: body.code,
|
||||
deviceId: String(body.device_id || headerDeviceId),
|
||||
});
|
||||
if (!verified.ok) return json(res, 403, failure(verified.error));
|
||||
return json(res, 200, verified);
|
||||
}
|
||||
|
||||
if (
|
||||
(req.method === "GET" || req.method === "DELETE")
|
||||
&& url.pathname === "/api/hololake/session"
|
||||
) {
|
||||
const authenticated = authenticateHoloLake(req);
|
||||
if (!authenticated.ok) {
|
||||
return json(
|
||||
res,
|
||||
authenticated.status,
|
||||
failure(authenticated.error),
|
||||
);
|
||||
}
|
||||
if (req.method === "DELETE") {
|
||||
const revoked = hololakeSessionManager.revoke(
|
||||
authenticated.token,
|
||||
authenticated.deviceId,
|
||||
);
|
||||
return json(res, revoked.ok ? 200 : 401, revoked);
|
||||
}
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
session: authenticated.session,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
req.method === "GET"
|
||||
&& url.pathname === "/api/hololake/knowledge/manifest"
|
||||
) {
|
||||
const authenticated = authenticateHoloLake(req);
|
||||
if (!authenticated.ok) {
|
||||
return json(
|
||||
res,
|
||||
authenticated.status,
|
||||
failure(authenticated.error),
|
||||
);
|
||||
}
|
||||
if (!hololakeKnowledgeProvider) {
|
||||
return json(res, 503, failure("knowledge_repository_unavailable"));
|
||||
}
|
||||
try {
|
||||
return json(res, 200, hololakeKnowledgeProvider.manifest());
|
||||
} catch {
|
||||
return json(res, 503, failure("knowledge_repository_unavailable"));
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
req.method === "GET"
|
||||
&& url.pathname === "/api/hololake/knowledge/archive"
|
||||
) {
|
||||
const authenticated = authenticateHoloLake(req);
|
||||
if (!authenticated.ok) {
|
||||
return json(
|
||||
res,
|
||||
authenticated.status,
|
||||
failure(authenticated.error),
|
||||
);
|
||||
}
|
||||
if (!hololakeKnowledgeProvider) {
|
||||
return json(res, 503, failure("knowledge_repository_unavailable"));
|
||||
}
|
||||
try {
|
||||
const archive = hololakeKnowledgeProvider.archive(
|
||||
url.searchParams.get("commit"),
|
||||
);
|
||||
res.writeHead(200, {
|
||||
"content-type": archive.content_type,
|
||||
"content-length": archive.body.length,
|
||||
"content-disposition": `attachment; filename="hololake-knowledge-${archive.commit}.zip"`,
|
||||
"cache-control": "private, no-store",
|
||||
"x-content-type-options": "nosniff",
|
||||
"x-hololake-commit": archive.commit,
|
||||
"x-content-sha256": archive.sha256,
|
||||
});
|
||||
res.end(archive.body);
|
||||
return;
|
||||
} catch (error) {
|
||||
const code = error && error.message === "knowledge_commit_not_current"
|
||||
? "knowledge_commit_not_current"
|
||||
: "knowledge_archive_unavailable";
|
||||
return json(res, code === "knowledge_commit_not_current" ? 409 : 503, failure(code));
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
req.method === "GET"
|
||||
&& url.pathname === "/api/hololake/ai/catalog"
|
||||
) {
|
||||
const authenticated = authenticateHoloLake(req);
|
||||
if (!authenticated.ok) {
|
||||
return json(
|
||||
res,
|
||||
authenticated.status,
|
||||
failure(authenticated.error),
|
||||
);
|
||||
}
|
||||
if (!hololakeAiGateway) {
|
||||
return json(res, 503, failure("ai_gateway_unavailable"));
|
||||
}
|
||||
return json(res, 200, hololakeAiGateway.catalog());
|
||||
}
|
||||
|
||||
if (
|
||||
req.method === "POST"
|
||||
&& url.pathname === "/api/hololake/ai/execute"
|
||||
) {
|
||||
const authenticated = authenticateHoloLake(req);
|
||||
if (!authenticated.ok) {
|
||||
return json(
|
||||
res,
|
||||
authenticated.status,
|
||||
failure(authenticated.error),
|
||||
);
|
||||
}
|
||||
if (!hololakeAiGateway) {
|
||||
return json(res, 503, failure("ai_gateway_unavailable"));
|
||||
}
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
try {
|
||||
return json(res, 200, await hololakeAiGateway.execute(body));
|
||||
} catch (error) {
|
||||
const code = String(error && error.message || "ai_gateway_failed");
|
||||
const status = code.startsWith("ai_upstream_") ? 502 : 400;
|
||||
return json(res, status, failure(code));
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/api/repositories/resolve") {
|
||||
const body = await readJson(req);
|
||||
if (!body) return json(res, 400, failure("invalid_json"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue