@bmqube/xmlrpc
Version:
A pure TypeScript XML-RPC client and server. Forked from (https://github.com/baalexander/node-xmlrpc)
174 lines (173 loc) • 7 kB
JavaScript
// Client.mts
import * as http from "http";
import * as https from "https";
import { parse as parseUrl } from "url";
import Serializer from "./serializer.mjs";
import Deserializer from "./deserializer.mjs";
import Cookies from "./cookies.mjs";
export default class Client {
constructor(options, isSecure = false) {
// Allow calling without `new`
// (keeping original API shape; TS classes require `new`, but we mimic by returning a new instance)
if (!(this instanceof Client)) {
return new Client(options, isSecure);
}
// Normalize options
let opts = typeof options === "string" ? (() => {
const parsed = parseUrl(options);
return {
host: parsed.hostname,
path: parsed.pathname ?? undefined,
port: parsed.port ? Number(parsed.port) : undefined,
};
})() : { ...options };
if (typeof opts.url !== "undefined") {
const parsedUrl = parseUrl(opts.url);
opts.host = parsedUrl.hostname ?? opts.host;
opts.path = parsedUrl.pathname ?? opts.path;
if (parsedUrl.port)
opts.port = Number(parsedUrl.port);
}
// Default headers
const defaultHeaders = {
"User-Agent": "NodeJS XML-RPC Client",
"Content-Type": "text/xml",
Accept: "text/xml",
"Accept-Charset": "UTF8",
Connection: "Keep-Alive",
};
opts.headers = opts.headers ?? {};
// Basic auth header
if (opts.headers.Authorization == null &&
opts.basic_auth?.user != null &&
opts.basic_auth?.pass != null) {
const auth = `${opts.basic_auth.user}:${opts.basic_auth.pass}`;
opts.headers["Authorization"] =
"Basic " + Buffer.from(auth).toString("base64");
}
for (const k of Object.keys(defaultHeaders)) {
if (opts.headers[k] === undefined) {
opts.headers[k] = defaultHeaders[k];
}
}
// Ensure method
opts.method = "POST";
// Fill some optional defaults for strong typing
const finalized = {
...opts,
_defaultAgent: opts._defaultAgent,
defaultPort: opts.defaultPort,
family: opts.family ?? 0,
hints: opts.hints ?? 0,
insecureHTTPParser: opts.insecureHTTPParser,
localPort: opts.localPort,
lookup: opts.lookup,
setDefaultHeaders: opts.setDefaultHeaders,
socketPath: opts.socketPath,
uniqueHeaders: opts.uniqueHeaders,
joinDuplicateHeaders: opts.joinDuplicateHeaders,
// Provide defaults for optional fields to satisfy Required<>
headers: opts.headers,
method: opts.method ?? "POST",
protocol: opts.protocol ?? undefined,
auth: opts.auth ?? undefined,
agent: opts.agent ?? undefined,
timeout: opts.timeout ?? undefined,
localAddress: opts.localAddress ?? undefined,
createConnection: opts.createConnection ?? undefined,
setHost: opts.setHost ?? undefined,
maxHeaderSize: opts.maxHeaderSize ?? undefined,
signal: opts.signal ?? undefined,
// Custom fields:
url: opts.url,
cookies: opts.cookies ?? false,
basic_auth: opts.basic_auth,
encoding: opts.encoding ?? "utf8",
responseEncoding: opts.responseEncoding,
// Keep other RequestOptions fields if present
host: opts.host,
hostname: opts.hostname,
port: opts.port,
path: opts.path,
};
this.options = finalized;
this.isSecure = isSecure;
this.headersProcessors = {
processors: [],
composeRequest: function (headers) {
this.processors.forEach((p) => p.composeRequest(headers));
},
parseResponse: function (headers) {
this.processors.forEach((p) => p.parseResponse(headers));
},
};
if (finalized.cookies) {
this.cookies = new Cookies();
this.headersProcessors.processors.unshift(this.cookies);
}
}
/**
* Makes an XML-RPC call to the server specified by the constructor's options.
*
* @param method The method name.
* @param params Params to send in the call.
* @param callback function(error, value) { ... }
*/
methodCall(method, params, callback) {
const options = this.options;
const xml = Serializer.serializeMethodCall(method, params, options.encoding);
const transport = this.isSecure ? https : http;
options.headers["Content-Length"] = Buffer.byteLength(xml, "utf8");
this.headersProcessors.composeRequest(options.headers);
const request = transport.request(options, (response) => {
const body = [];
response.on("data", (chunk) => {
body.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
});
const __enrichError = (err) => {
// mirror original non-enumerable properties
Object.defineProperty(err, "req", { value: request });
Object.defineProperty(err, "res", { value: response });
Object.defineProperty(err, "body", { value: Buffer.concat(body).toString() });
return err;
};
if (response.statusCode === 404) {
callback(__enrichError(new Error("Not Found")));
}
else {
this.headersProcessors.parseResponse(response.headers);
console.log({ response: JSON.stringify(response, null, 2) });
const deserializer = new Deserializer(options.responseEncoding);
deserializer.deserializeMethodResponse(response, (err, result) => {
if (err)
err = __enrichError(err);
callback(err, result);
});
}
});
request.on("error", callback);
request.write(xml, "utf8");
request.end();
}
/**
* Gets the cookie value by its name.
* Throws if cookies were not enabled on this client.
*/
getCookie(name) {
if (!this.cookies) {
throw new Error("Cookies support is not turned on for this client instance");
}
return this.cookies.get(name);
}
/**
* Sets the cookie value by its name (sent on the next XML-RPC call).
* Chainable.
*/
setCookie(name, value) {
if (!this.cookies) {
throw new Error("Cookies support is not turned on for this client instance");
}
this.cookies.set(name, value);
return this;
}
}