<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #45862 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - Already added in the previous PR. ## Testing - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved detection and reporting of enrollment errors sent by the server, ensuring error messages surface reliably. * Prevented streaming leaks by stopping background work when a client disconnects. * **Documentation / Protocol** * Made server-to-client streaming more spec-compliant (framing, heartbeats) for more robust Android Enterprise enrollment communication. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/45988?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
import sendRequest from "services";
|
|
import endpoints from "utilities/endpoints";
|
|
import authToken from "utilities/auth_token";
|
|
|
|
interface IGetAndroidSignupUrlResponse {
|
|
android_enterprise_signup_url: string;
|
|
}
|
|
|
|
interface IGetAndroidEnterpriseResponse {
|
|
android_enterprise_id: boolean;
|
|
}
|
|
|
|
export default {
|
|
getSignupUrl: (): Promise<IGetAndroidSignupUrlResponse> => {
|
|
const { MDM_ANDROID_SIGNUP_URL } = endpoints;
|
|
return sendRequest("GET", MDM_ANDROID_SIGNUP_URL);
|
|
},
|
|
|
|
getAndroidEnterprise: (): Promise<IGetAndroidEnterpriseResponse> => {
|
|
const { MDM_ANDROID_ENTERPRISE } = endpoints;
|
|
return sendRequest("GET", MDM_ANDROID_ENTERPRISE);
|
|
},
|
|
|
|
turnOffAndroidMdm: (): Promise<void> => {
|
|
const { MDM_ANDROID_ENTERPRISE } = endpoints;
|
|
return sendRequest("DELETE", MDM_ANDROID_ENTERPRISE);
|
|
},
|
|
|
|
/**
|
|
* This function starts a Server-Sent Events connection with the fleet server
|
|
* to get messages about a successful Android mdm connection. We have to use
|
|
* fetch here because the EventSource API does not support setting headers,
|
|
* which we need to authenticate the request.
|
|
*/
|
|
startSSE: (abortSignal: AbortSignal): Promise<void> => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const response = await fetch(endpoints.MDM_ANDROID_SSE_URL, {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${authToken.get()}`,
|
|
Accept: "text/event-stream",
|
|
},
|
|
signal: abortSignal,
|
|
});
|
|
|
|
const reader = response?.body?.getReader();
|
|
if (!reader) {
|
|
reject(new Error("Android MDM SSE stream unavailable"));
|
|
return;
|
|
}
|
|
const decoder = new TextDecoder();
|
|
const successSignal = "data: Android Enterprise successfully connected";
|
|
const errorMarker = "event: error";
|
|
// Buffer accumulates decoded text so a frame split across multiple
|
|
// chunks (valid with chunked transfer encoding) is still detected.
|
|
const tailLen = Math.max(successSignal.length, errorMarker.length);
|
|
let buffer = "";
|
|
let loggedError = false;
|
|
|
|
while (true) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
// Server closed the stream without ever sending success.
|
|
// Reject so callers don't await forever on unmount or backend hiccup.
|
|
reject(new Error("Android MDM SSE ended before success signal"));
|
|
return;
|
|
}
|
|
buffer += decoder.decode(value, { stream: true });
|
|
// Surface backend-emitted error events. Chrome's EventStream tab
|
|
// doesn't attach to fetch-based SSE reliably, so without this log
|
|
// the only signal of a server-side failure is the generic UI
|
|
// toast.
|
|
if (!loggedError && buffer.includes(errorMarker)) {
|
|
console.error("[android-sse]:", buffer);
|
|
loggedError = true;
|
|
}
|
|
if (buffer.includes(successSignal)) {
|
|
resolve();
|
|
return;
|
|
}
|
|
buffer = buffer.slice(-tailLen);
|
|
}
|
|
} catch (error) {
|
|
if ((error as Error).name === "AbortError") {
|
|
// we want to ignore abort errors
|
|
console.error("SSE Fetch aborted");
|
|
} else {
|
|
reject(error);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|