UNPKG

chia-agent

Version:
354 lines (353 loc) 16.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RPCAgent = void 0; exports.getConnectionInfoFromConfig = getConnectionInfoFromConfig; exports.getConf = getConf; exports.loadCertFilesFromConfig = loadCertFilesFromConfig; const https_1 = require("https"); const http_1 = require("http"); const fs_1 = require("fs"); const JSONbigBuilder = require("@chiamine/json-bigint"); const logger_1 = require("../logger"); const index_1 = require("../config/index"); const JSONbig = JSONbigBuilder({ useNativeBigInt: true, alwaysParseAsBig: false, }); function getConnectionInfoFromConfig(destination, config) { let hostname = "localhost"; let port = -1; if (destination === "daemon") { port = +config["/daemon_port"]; } else if (destination === "farmer") { port = +config["/farmer/rpc_port"]; } else if (destination === "harvester") { port = +config["/harvester/rpc_port"]; } else if (destination === "full_node") { port = +config["/full_node/rpc_port"]; } else if (destination === "wallet") { port = +config["/wallet/rpc_port"]; } else if (destination === "data_layer") { port = +config["/data_layer/rpc_port"]; } else if (destination === "pool") { const poolUrl = config["/pool/pool_list/0/pool_url"]; const regex = /^(https?:\/\/)?([^/:]+):?(\d*)/; const match = regex.exec(poolUrl); if (match) { hostname = match[2]; port = match[3] ? +match[3] : 80; } else { (0, logger_1.getLogger)().error("Pool list was not found in config.yaml."); throw new Error("Pool list was not found in config.yaml"); } } else { throw new Error(`Unknown destination: ${destination}`); } return { hostname, port }; } function getConf(configPath) { configPath = configPath || index_1.configPath; if (!(0, fs_1.existsSync)(configPath)) { (0, logger_1.getLogger)().error(`chia config file does not exist at: ${configPath}`); throw new Error("chia config file Not Found."); } return (0, index_1.getConfig)(configPath); } function loadCertFilesFromConfig(config) { const clientCertPath = (0, index_1.resolveFromChiaRoot)([ config["/daemon_ssl/private_crt"], ]); const clientKeyPath = (0, index_1.resolveFromChiaRoot)([ config["/daemon_ssl/private_key"], ]); const caCertPath = (0, index_1.resolveFromChiaRoot)([ config["/private_ssl_ca/crt"], ]); (0, logger_1.getLogger)().debug(() => `Loading client cert file from ${clientCertPath}`); (0, logger_1.getLogger)().debug(() => `Loading client key file from ${clientKeyPath}`); (0, logger_1.getLogger)().debug(() => `Loading ca cert file from ${caCertPath}`); const getCertOrKey = (path) => { if (!(0, fs_1.existsSync)(path)) { (0, logger_1.getLogger)().error(`ssl crt/key does not exist at: ${path}`); throw new Error(`crt/key Not Found at ${path}`); } return (0, fs_1.readFileSync)(path); }; const clientCert = getCertOrKey(clientCertPath); const clientKey = getCertOrKey(clientKeyPath); const caCert = getCertOrKey(caCertPath); return { clientCert, clientKey, caCert }; } const userAgent = "chia-agent/1.0.0"; class RPCAgent { constructor(props) { this._skip_hostname_verification = false; this._host = ""; this._port = 0; if ("httpsAgent" in props) { this._protocol = "https"; this._agent = props.httpsAgent; this._skip_hostname_verification = Boolean(props.skip_hostname_verification); // Extract host/port from httpsAgent options // Note: TypeScript doesn't expose options property, but it exists at runtime const agent = this._agent; if (agent.options && agent.options.host && agent.options.port) { this._host = agent.options.host; this._port = agent.options.port; (0, logger_1.getLogger)().debug(() => `Constructing RPCAgent with httpsAgent: ${this._host}:${this._port}`); } else { (0, logger_1.getLogger)().debug(() => "Constructing RPCAgent with httpsAgent (host/port not available in agent options)"); } } else if ("httpAgent" in props) { this._protocol = "http"; this._agent = props.httpAgent; this._host = props.host; this._port = props.port; this._skip_hostname_verification = Boolean(props.skip_hostname_verification); (0, logger_1.getLogger)().debug(() => `Constructing RPCAgent with httpAgent: ${this._host}:${this._port}`); } else if ("protocol" in props) { this._protocol = props.protocol; const { host, port } = props; let clientCert; let clientKey; let caCert; const keepAlive = props.keepAlive !== false; const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0 ? props.keepAliveMsecs : 1000; const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0 ? props.maxSockets : Infinity; const timeout = typeof props.timeout === "number" && props.timeout > 0 ? props.timeout : undefined; this._host = host; this._port = port; if (props.protocol === "https") { if ("configPath" in props) { const config = getConf(props.configPath); const certs = loadCertFilesFromConfig(config); clientCert = certs.clientCert; clientKey = certs.clientKey; caCert = certs.caCert; this._skip_hostname_verification = Boolean(props.skip_hostname_verification); } else { ({ client_cert: clientCert, client_key: clientKey, ca_cert: caCert, } = props); this._skip_hostname_verification = Boolean(props.skip_hostname_verification); } this._agent = new https_1.Agent({ host, port, ca: caCert, cert: clientCert, key: clientKey, rejectUnauthorized: Boolean(caCert) && host !== "localhost", keepAlive, keepAliveMsecs, maxSockets, timeout, }); (0, logger_1.getLogger)().debug(() => `Constructed RPCAgent with httpsAgent: ${host}:${port}`); } else { this._agent = new http_1.Agent({ keepAlive, keepAliveMsecs, maxSockets, timeout, }); } } else { this._protocol = "https"; let host; let port; const keepAlive = props.keepAlive !== false; const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0 ? props.keepAliveMsecs : 1000; const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0 ? props.maxSockets : Infinity; const timeout = typeof props.timeout === "number" && props.timeout > 0 ? props.timeout : undefined; const config = getConf("configPath" in props ? props.configPath : undefined); if (props.host && typeof props.port === "number") { ({ host, port } = props); } else { const info = getConnectionInfoFromConfig(props.service, config); host = props.host ? props.host : info.hostname; port = typeof props.port === "number" ? props.port : info.port; } (0, logger_1.getLogger)().debug(() => `Picked ${host}:${port} for ${props.service}`); const certs = loadCertFilesFromConfig(config); const { clientCert, clientKey, caCert } = certs; this._skip_hostname_verification = Boolean(props.skip_hostname_verification); this._host = host; this._port = port; this._agent = new https_1.Agent({ host: host, port: port, ca: caCert, cert: clientCert, key: clientKey, rejectUnauthorized: Boolean(caCert) && host !== "localhost", keepAlive, keepAliveMsecs, maxSockets, timeout, }); } } async sendMessage(destination, command, data) { // parameter `destination` is not used because target rpc server is determined by url. (0, logger_1.getLogger)().debug(() => `Sending RPC message. dest=${destination} command=${command}`); return this.request("POST", command, data); } async request(method, path, data) { return new Promise((resolve, reject) => { const body = data ? JSONbig.stringify(data) : "{}"; const pathname = `/${path.replace(/^\/+/, "")}`; const METHOD = method.toUpperCase(); const headers = { Accept: "application/json, text/plain, */*", "User-Agent": userAgent, }; if (this._host) { headers.Host = this._host; } const options = { path: pathname, method: METHOD, agent: this._agent, headers, }; // For HTTP protocol, we need to explicitly set hostname and port if (this._protocol === "http") { options.hostname = this._host; options.port = this._port; } if (this._skip_hostname_verification) { options.checkServerIdentity = () => { return undefined; }; } if (METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") { options.headers = { ...(options.headers || {}), "Content-Type": "application/json;charset=utf-8", "Content-Length": body.length, }; } else if (METHOD === "GET") { // Add query string if `data` is object. if (data && typeof data === "object") { // Remove string after '?' on path to prevent duplication. let p = options.path; if (/\?/.test(p)) { (0, logger_1.getLogger)().warning("querystring in `path` is replaced by `data`"); p.replace(/\?.*/, ""); } p += "?"; for (const key in data) { if (Object.prototype.hasOwnProperty.call(data, key)) { p += `${key}=${data[key]}`; } } options.path = p; } } const transporter = this._protocol === "https" ? https_1.request : http_1.request; (0, logger_1.getLogger)().debug(() => `Dispatching RPC ${METHOD} request to ${this._protocol}//${this._host}:${this._port}${options.path}`); (0, logger_1.getLogger)().trace(() => `Request options: ${(0, logger_1.stringify)(options)}`); (0, logger_1.getLogger)().trace(() => `Request body: ${body}`); const req = transporter(options, (res) => { if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) { (0, logger_1.getLogger)().error(`Status not ok: ${res.statusCode}`); if (res.statusCode === 404) { (0, logger_1.getLogger)().error("Maybe the RPCAgent is connecting to different service against target rpc command."); (0, logger_1.getLogger)().error("For example, this happens when invoking 'new_farming_info' rpc command" + " to 'full_node' service, which 'farm' service is correct"); (0, logger_1.getLogger)().error("Check invoking command is correct and connecting service/host is right for the command"); } return reject(new Error(`Status not ok: ${res.statusCode}`)); } const chunks = []; res.on("data", (chunk) => { chunks.push(chunk); if (chunks.length === 0) { (0, logger_1.getLogger)().debug(() => "The first response chunk data arrived"); } (0, logger_1.getLogger)().trace(() => `Response chunk #${chunks.length} - ${chunk.length} bytes: ${chunk.toString()}`); }); res.on("end", () => { try { if (chunks.length > 0) { const entireChunks = Buffer.concat(chunks); const serializedData = entireChunks.toString(); const d = JSONbig.parse(serializedData); if (typeof d !== "object" || !d) { (0, logger_1.getLogger)().error(`Response is expected to be an object but received: ${serializedData}`); return reject(new Error(`Unexpected response format: ${serializedData}`)); } else if (!Object.prototype.hasOwnProperty.call(d, "success")) { (0, logger_1.getLogger)().error("Response has no 'success' property"); return reject(new Error(`Response has no 'success' property: ${serializedData}`)); } if (!d.success) { (0, logger_1.getLogger)().info(`API failure: ${d.error}`); return reject(d); } (0, logger_1.getLogger)().debug(() => `RPC response received from ${this._protocol}//${this._host}:${this._port}${options.path}`); (0, logger_1.getLogger)().trace(() => `RPC response data: ${(0, logger_1.stringify)(d)}`); return resolve(d); } // RPC Server should return response like // {origin: string; destination: string; request_id: string; data: any; ...} // If no such response is returned, reject it. (0, logger_1.getLogger)().error("RPC Server returned no data. This is not expected."); reject(new Error("Server responded without expected data")); } catch (e) { (0, logger_1.getLogger)().error(`Failed to parse response data: ${(0, logger_1.stringify)(e)}`); try { (0, logger_1.getLogger)().error(Buffer.concat(chunks).toString()); } catch (_e2) { /* Do nothing */ } reject(new Error("Server responded without expected data")); } }); }); req.on("error", (error) => { (0, logger_1.getLogger)().error(() => (0, logger_1.stringify)(error)); reject(error); }); if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") && body) { req.write(body); } req.end(); }); } } exports.RPCAgent = RPCAgent;