UNPKG

@fdm-monster/server

Version:

FDM Monster is a bulk OctoPrint, Klipper, PrusaLink and BambuLab manager to set up, configure and monitor 3D printers. Our aim is to provide neat overview over your farm.

128 lines (127 loc) 5.2 kB
import { authorizationHeaderKey, wwwAuthenticationHeaderKey } from "../../octoprint/constants/octoprint-service.constants.js"; import { DefaultHttpClientBuilder } from "../../../shared/default-http-client.builder.js"; import { generateDigestAuthHeader } from "./digest-auth.util.js"; import { randomBytes } from "node:crypto"; //#region src/services/prusa-link/utils/prusa-link-http-client.builder.ts var PrusaLinkHttpClientBuilder = class extends DefaultHttpClientBuilder { maxRetries = 1; username; password; authHeaderContext; onAuthError; onAuthSuccess; onRequestRetry; build() { if (!this.axiosOptions.baseURL) throw new Error("Base URL is required"); const axiosInstance = super.build(); if (this.username && this.password) { axiosInstance.interceptors.request.use(async (config) => { if (this.authHeaderContext) { const method = config.method?.toUpperCase() ?? "GET"; const rawUrl = config.url ?? "/"; let uri = rawUrl; if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) try { const parsed = new URL(rawUrl); uri = `${parsed.pathname}${parsed.search ?? ""}`; } catch {} config.headers[authorizationHeaderKey] = this.generateDigestHeader(method, uri); } return config; }); axiosInstance.interceptors.response.use((response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && this.username?.length && this.password?.length && (!originalRequest._retryCount || originalRequest._retryCount < this.maxRetries)) { const wwwAuthHeader = error.response.headers[wwwAuthenticationHeaderKey]; if (wwwAuthHeader) { if (typeof this.onAuthSuccess === "function") this.onAuthSuccess(wwwAuthHeader); this.saveParsedAuthHeaderContext(wwwAuthHeader); originalRequest._retryCount = (originalRequest._retryCount ?? 0) + 1; if (typeof this.onRequestRetry === "function") this.onRequestRetry(error, originalRequest._retryCount); return axiosInstance(originalRequest); } } if (error.response?.status === 401 && this.onAuthError && typeof this.onAuthError === "function") this.onAuthError(error); return Promise.reject(error); }); } return axiosInstance; } withDigestAuth(username, password, onAuthError, onRequestRetry, onAuthSuccess) { if (!username?.length) throw new Error("username may not be an empty string"); if (!password?.length) throw new Error("password may not be an empty string"); if (onAuthError && typeof onAuthError !== "function") throw new Error("onAuthError must be a function"); if (onAuthSuccess && typeof onAuthSuccess !== "function") throw new Error("onAuthSuccess must be a function"); if (onRequestRetry && typeof onRequestRetry !== "function") throw new Error("onRequestRetry must be a function"); this.username = username; this.password = password; this.onAuthError = onAuthError; this.onRequestRetry = onRequestRetry; this.onAuthSuccess = onAuthSuccess; return this; } withAuthHeader(authHeader) { if (!authHeader?.length) throw new Error("Digest header may not be an empty string"); this.saveParsedAuthHeaderContext(authHeader); return this; } saveParsedAuthHeaderContext(authHeader) { const cleanedHeader = authHeader.trim(); const headerValue = cleanedHeader.startsWith("Digest ") ? cleanedHeader.substring(7) : cleanedHeader; const tokens = []; let current = ""; let inQuotes = false; for (const ch of headerValue) { if (ch === "\"") { inQuotes = !inQuotes; current += ch; continue; } if (ch === "," && !inQuotes) { if (current.trim().length) tokens.push(current.trim()); current = ""; continue; } current += ch; } if (current.trim().length) tokens.push(current.trim()); const authParams = Object.fromEntries(tokens.map((param) => { const idx = param.indexOf("="); if (idx === -1) return [param.trim(), ""]; const key = param.slice(0, idx).trim(); let value = param.slice(idx + 1).trim(); if (value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1); return [key, value]; })); const qopRaw = authParams.qop; const qop = qopRaw ? qopRaw.split(",").map((q) => q.trim()).find((q) => q === "auth") ?? qopRaw.split(",")[0].trim() : void 0; this.authHeaderContext = { realm: authParams.realm, nonce: authParams.nonce, qop, hasQop: !!qop, opaque: authParams.opaque, algorithm: authParams.algorithm }; } generateDigestHeader(method, uri) { if (!this.authHeaderContext || !this.username || !this.password) throw new Error("Digest auth not properly configured"); const { realm, nonce, qop, hasQop, opaque, algorithm } = this.authHeaderContext; const cnonce = hasQop || algorithm?.toLowerCase() === "md5-sess" ? randomBytes(8).toString("hex") : void 0; return generateDigestAuthHeader({ username: this.username, password: this.password, method, uri, realm, nonce, qop: hasQop ? qop : void 0, nc: hasQop ? "00000001" : void 0, cnonce, opaque, algorithm }); } }; //#endregion export { PrusaLinkHttpClientBuilder }; //# sourceMappingURL=prusa-link-http-client.builder.js.map