@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
JavaScript
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