UNPKG

@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
// 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; } }