UNPKG

monetdb

Version:
874 lines 33.9 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryStream = exports.HandShakeOption = exports.createMapiConfig = exports.parseMapiUri = exports.MapiConnection = void 0; const node_net_1 = require("node:net"); const events_1 = require("events"); const buffer_1 = require("buffer"); const node_crypto_1 = require("node:crypto"); const defaults_1 = __importDefault(require("./defaults")); const node_url_1 = require("node:url"); const file_transfer_1 = require("./file-transfer"); const MAPI_BLOCK_SIZE = 1024 * 8 - 2; const MAPI_HEADER_SIZE = 2; const MSG_PROMPT = ""; const MSG_MORE = "\x01\x02\n"; const MSG_FILETRANS = "\x01\x03\n"; const MSG_INFO = "#"; const MSG_ERROR = "!"; const MSG_Q = "&"; const MSG_QTABLE = "&1"; const MSG_QUPDATE = "&2"; const MSG_QSCHEMA = "&3"; const MSG_QTRANS = "&4"; const MSG_QPREPARE = "&5"; const MSG_QBLOCK = "&6"; const MSG_HEADER = "%"; const MSG_TUPLE = "["; const MSG_TUPLE_NOSLICE = "="; const MSG_REDIRECT = "^"; const MSG_OK = "=OK"; const MAX_REDIRECTS = 10; const MAX_BUFF_SIZE = buffer_1.constants.MAX_LENGTH; var MAPI_STATE; (function (MAPI_STATE) { MAPI_STATE[MAPI_STATE["INIT"] = 1] = "INIT"; MAPI_STATE[MAPI_STATE["CONNECTED"] = 2] = "CONNECTED"; MAPI_STATE[MAPI_STATE["READY"] = 3] = "READY"; })(MAPI_STATE || (MAPI_STATE = {})); var MAPI_LANGUAGE; (function (MAPI_LANGUAGE) { MAPI_LANGUAGE["SQL"] = "sql"; MAPI_LANGUAGE["MAPI"] = "mapi"; MAPI_LANGUAGE["CONTROL"] = "control"; })(MAPI_LANGUAGE || (MAPI_LANGUAGE = {})); class HandShakeOption { constructor(level, name, value, fallback, sent = false) { this.level = level; this.name = name; this.value = value; this.fallback = fallback; this.sent = sent; } } exports.HandShakeOption = HandShakeOption; function isMapiUri(uri) { const regx = new RegExp("^mapi:monetdb://*", "i"); return regx.test(uri); } function parseMapiUri(uri) { if (isMapiUri(uri)) { const url = new node_url_1.URL(uri.substring(5)); if (url.hostname) { const host = url.hostname; const port = parseInt(url.port); const username = url.username; const password = url.password; const database = url.pathname.split("/")[1]; return { host, port, username, password, database, }; } } throw new Error(`Invalid MAPI URI ${uri}!`); } exports.parseMapiUri = parseMapiUri; // validates and sets defaults on missing properties function createMapiConfig(params) { const database = params && params.database ? params.database : defaults_1.default.database; if (typeof database != "string") { throw new Error("database name must be string"); } const username = params && params.username ? params.username : defaults_1.default.username; const password = params && params.password ? params.password : defaults_1.default.password; let host = params && params.host; const unixSocket = params && params.unixSocket; if (!unixSocket && !host) host = defaults_1.default.host; if (typeof host != "string") { throw new TypeError(`${host} is not valid hostname`); } const port = params && params.port ? Number(params.port) : Number(defaults_1.default.port); if (isNaN(port)) { throw new TypeError(`${port} is not valid port`); } const timeout = params && params.timeout ? Number(params.timeout) : undefined; if (timeout && isNaN(timeout)) { throw new TypeError("timeout must be number"); } const language = params && params.language ? params.language : MAPI_LANGUAGE.SQL; const autoCommit = params.autoCommit || defaults_1.default.autoCommit; const replySize = params.replySize || defaults_1.default.replySize; return { database, username, password, language, host, port, timeout, unixSocket, autoCommit, replySize, }; } exports.createMapiConfig = createMapiConfig; class Column { constructor(table, name, type, index, length) { this.table = table; this.name = name; this.type = type; this.index = index; this.length = length; } } class QueryStream extends events_1.EventEmitter { constructor() { super(); } end(res) { this.emit("end", res); } } exports.QueryStream = QueryStream; function parseHeaderLine(hdrLine) { if (hdrLine.startsWith(MSG_HEADER)) { const [head, tail] = hdrLine.substring(1).trim().split("#"); let res = {}; const vals = head.trim().split(",\t"); switch (tail.trim()) { case "table_name": res = { tableNames: vals }; break; case "name": res = { columnNames: vals }; break; case "type": res = { columnTypes: vals }; break; default: res = {}; } return res; } throw TypeError("Invalid header format!"); } function parseTupleLine(line, types) { if (line.startsWith(MSG_TUPLE) && line.endsWith("]")) { var resultline = []; var cCol = 0; var curtok = ""; var state = "INCRAP"; let endQuotes = 0; /* mostly adapted from clients/R/MonetDB.R/src/mapisplit.c */ for (var curPos = 2; curPos < line.length - 1; curPos++) { var chr = line.charAt(curPos); switch (state) { case "INCRAP": if (chr != "\t" && chr != "," && chr != " ") { if (chr == '"') { state = "INQUOTES"; } else { state = "INTOKEN"; curtok += chr; } } break; case "INTOKEN": if (chr == "," || curPos == line.length - 2) { if (curtok == "NULL" && endQuotes === 0) { resultline.push(null); } else { switch (types[cCol]) { case "boolean": resultline.push(curtok == "true"); break; case "tinyint": case "smallint": case "int": case "wrd": case "bigint": resultline.push(parseInt(curtok)); break; case "real": case "double": case "decimal": resultline.push(parseFloat(curtok)); break; case "json": try { resultline.push(JSON.parse(curtok)); } catch (e) { resultline.push(curtok); } break; default: // we need to unescape double quotes //valPtr = valPtr.replace(/[^\\]\\"/g, '"'); resultline.push(curtok); break; } } cCol++; state = "INCRAP"; curtok = ""; endQuotes = 0; } else { curtok += chr; } break; case "ESCAPED": state = "INQUOTES"; switch (chr) { case "t": curtok += "\t"; break; case "n": curtok += "\n"; break; case "r": curtok += "\r"; break; default: curtok += chr; } break; case "INQUOTES": if (chr == '"') { state = "INTOKEN"; endQuotes++; break; } if (chr == "\\") { state = "ESCAPED"; break; } curtok += chr; break; } } return resultline; } throw TypeError("Invalid tuple format!"); } class Response { constructor(opt = {}) { this.chunks = []; //this.buff = Buffer.allocUnsafe(MAPI_BLOCK_SIZE).fill(0); this.offset = 0; this.parseOffset = 0; this.settled = false; this.last = false; this.headerEmitted = false; this.stream = opt.stream; this.callbacks = opt.callbacks; this.fileHandler = opt.fileHandler; this.leftOver = 0; this._str = ""; if (opt.stream) { this.queryStream = new QueryStream(); if (opt.callbacks && opt.callbacks.resolve) opt.callbacks.resolve(this.queryStream); } } append(data) { //let srcStartIndx = 0; //let srcEndIndx = srcStartIndx + data.length; let bytesProcessed = 0; if (this.leftOver == 0) { // new mapi blk, read header const hdr = data.readUInt16LE(0); this.last = (hdr & 1) === 1; this.leftOver = hdr >> 1; data = data.subarray(MAPI_HEADER_SIZE); bytesProcessed += MAPI_HEADER_SIZE; //bytesCopied = data.copy( // this.buff, // this.offset, // srcStartIndx, // srcEndIndx //); //segment = new Segment(bytes, last, this.offset, bytesCopied); //this.segments.push(segment); //this.offset += bytesCopied; //bytesProcessed = MAPI_HEADER_SIZE + bytesCopied; } //else { // const byteCntToRead = segment.bytes - segment.bytesOffset; // srcEndIndx = srcStartIndx + byteCntToRead; // bytesCopied = data.copy( // this.buff, // this.offset, // srcStartIndx, // srcEndIndx // ); // this.offset += bytesCopied; // segment.bytesOffset += bytesCopied; // // console.log(`segment is full ${segment.bytesOffset === segment.bytes}`); // bytesProcessed = bytesCopied; //} const bytesToRead = Math.min(data.length, this.leftOver); //console.log(`bytesToRead ${bytesToRead}`); if (bytesToRead > 0) { const chunk = data.subarray(0, bytesToRead); this.chunks.push(chunk); this.leftOver -= bytesToRead; bytesProcessed += bytesToRead; } //if (this.isQueryResponse()) { // const tuples = []; // // const firstPackage = this.parseOffset === 0; // this.parseOffset += this.parseQueryResponse( // this.toString(this.parseOffset), // tuples // ); // console.log("tuples"); // console.log(tuples); // if (tuples.length > 0) { // if (this.queryStream) { // // emit header once // if ( // this.headerEmitted === false && // this.result && // this.result.columns // ) { // this.queryStream.emit("header", this.result.columns); // this.headerEmitted = true; // } // // emit tuples // this.queryStream.emit("data", tuples); // } else { // this.result.data = this.result.data || []; // for (let t of tuples) { // this.result.data.push(t); // } // } // } //} //console.log(`bytesProcessed ${bytesToRead}`); return bytesProcessed; } complete() { return this.last && (this.leftOver == 0); } errorMessage() { const msg = this.toString(); if (msg.startsWith(MSG_ERROR)) { return msg; } return ""; } isFileTransfer() { return this.toString().startsWith(MSG_FILETRANS); } isPrompt() { return this.complete() && this.toString() == MSG_PROMPT; } isRedirect() { return this.toString().startsWith(MSG_REDIRECT); } isQueryResponse() { if (this.result && this.result.type) { return this.result.type.startsWith(MSG_Q); } return this.toString().startsWith(MSG_Q); } isMsgMore() { // server wants more ? return this.toString().startsWith(MSG_MORE); } toString(start) { if (this.complete() && !this.queryStream && this._str) { if (start) return this._str.substring(start); return this._str; } const buff = buffer_1.Buffer.concat(this.chunks); this._str = buff.toString("utf8"); if (start) return this._str.substring(start); return this._str; } settle(res) { if (this.settled === false && this.complete()) { const errMsg = this.errorMessage(); const err = errMsg ? new Error(errMsg) : null; if (this.queryStream) { if (err) this.queryStream.emit("error", err); this.queryStream.end(); } else { if (this.callbacks) { if (err) { this.callbacks.reject(err); } else { this.callbacks.resolve(res || this.result); } } else if (this.fileHandler && this.isQueryResponse()) { this.fileHandler.resolve(this.result); } else if (this.fileHandler && (err || this.fileHandler.err)) { this.fileHandler.reject(err || this.fileHandler.err); } } this.settled = true; this._str = ""; this.chunks = []; } } parseQueryResponse(data, res) { let offset = 0; let eol = data.indexOf("\n"); let line = eol > 0 ? data.substring(0, eol) : undefined; while (line) { switch (line.charAt(0)) { case MSG_Q: // first line this.result = this.result || {}; this.result.type = line.substring(0, 2); const rest = line.substring(3).trim().split(" "); if (this.result.type === MSG_QTABLE) { const [id, rowCnt, columnCnt, rows, queryId, queryTime, malOptimizerTime, sqlOptimizerTime,] = rest; this.result.id = parseInt(id); this.result.rowCnt = parseInt(rowCnt); this.result.columnCnt = parseInt(columnCnt); this.result.queryId = parseInt(queryId); this.result.queryTime = parseInt(queryTime); this.result.malOptimizerTime = parseInt(malOptimizerTime); this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime); } else if (this.result.type === MSG_QUPDATE) { const [affectedRowCnt, autoIncrementId, queryId, queryTime, malOptimizerTime, sqlOptimizerTime,] = rest; this.result.affectedRows = parseInt(affectedRowCnt); this.result.queryId = parseInt(queryId); this.result.queryTime = parseInt(queryTime); this.result.malOptimizerTime = parseInt(malOptimizerTime); this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime); } else if (this.result.type === MSG_QSCHEMA) { const [queryTime, malOptimizerTime] = rest; this.result.queryTime = parseInt(queryTime); this.result.malOptimizerTime = parseInt(malOptimizerTime); } else if (this.result.type === MSG_QTRANS) { // skip } else if (this.result.type === MSG_QPREPARE) { const [id, rowCnt, columnCnt, rows] = rest; this.result.id = parseInt(id); this.result.rowCnt = parseInt(rowCnt); this.result.columnCnt = parseInt(columnCnt); } break; case MSG_HEADER: const header = parseHeaderLine(line); if (this.result.headers !== undefined) { this.result.headers = Object.assign(Object.assign({}, this.result.headers), header); } else { this.result.headers = header; } // if we have all headers we can compile column info const haveAllHeaders = Boolean(this.result.headers.tableNames) && Boolean(this.result.headers.columnNames) && Boolean(this.result.headers.columnTypes); if (this.result.columns === undefined && haveAllHeaders) { const colums = []; for (let i = 0; i < this.result.columnCnt; i++) { const table = this.result.headers.tableNames[i]; const name = this.result.headers.columnNames[i]; const type = this.result.headers.columnTypes[i]; colums.push({ table, name, type, index: i, }); } this.result.columns = colums; } break; case MSG_TUPLE: const tuple = parseTupleLine(line, this.result.headers.columnTypes); res.push(tuple); break; default: throw TypeError(`Invalid query response line!\n${line}`); } // line is processed advance offset offset = eol + 1; // get next line eol = data.indexOf("\n", offset); line = eol > 0 ? data.substring(offset, eol) : undefined; } return offset; } streamResult() { if (this.queryStream && this.isQueryResponse()) { const tuples = []; this.parseOffset += this.parseQueryResponse(this.toString(this.parseOffset), tuples); if (tuples.length > 0) { // emit header once if (this.headerEmitted === false && this.result && this.result.columns) { this.queryStream.emit("header", this.result.columns); this.headerEmitted = true; } // emit tuples this.queryStream.emit("data", tuples); } } } } class MapiConnection extends events_1.EventEmitter { constructor(config) { super(); this.createSocket = (timeout) => { const socket = new node_net_1.Socket(); if (timeout) socket.setTimeout(timeout); socket.addListener("data", this.recv.bind(this)); socket.addListener("error", this.handleSocketError.bind(this)); socket.addListener("timeout", this.handleTimeout.bind(this)); socket.once("end", () => { console.log("Server has ended the connection"); }); return socket; }; this.state = MAPI_STATE.INIT; this.redirects = 0; this.queue = []; this.database = config.database; this.language = config.language || MAPI_LANGUAGE.SQL; this.unixSocket = config.unixSocket; this.host = config.host; this.port = config.port; this.username = config.username; this.password = config.password; this.timeout = config.timeout; } connect(handShakeOptions = []) { this.handShakeOptions = handShakeOptions; // TODO unix socket const opt = { port: this.port, host: this.host, noDelay: true, }; const socket = this.socket && !this.socket.destroyed ? this.socket : this.createSocket(this.timeout); socket.connect(opt, () => { this.state = MAPI_STATE.CONNECTED; this.socket.setKeepAlive(true); }); this.socket = socket; return (0, events_1.once)(this, "ready"); } ready() { return this.socket && this.socket.writable && this.state === MAPI_STATE.READY; } disconnect() { return new Promise((resolve, reject) => { if (!this.socket || this.socket.destroyed) { this.socket = null; this.state = MAPI_STATE.INIT; return resolve(true); } const onClose = () => { cleanup(); this.socket = null; this.state = MAPI_STATE.INIT; resolve(true); }; const onError = (err) => { var _a; console.error(err); cleanup(); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.destroy(); // force cleanup this.socket = null; this.state = MAPI_STATE.INIT; reject(err); }; const cleanup = () => { var _a, _b; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeListener("close", onClose); (_b = this.socket) === null || _b === void 0 ? void 0 : _b.removeListener("error", onError); }; this.socket.once("close", onClose); this.socket.once("error", onError); // Initiate graceful shutdown this.socket.end(); }); } destroy() { if (this.socket && !this.socket.destroyed) this.socket.destroy(); this.socket = null; } login(challenge) { const challengeParts = challenge.split(":"); const [salt, identity, protocol, hashes, endian, algo, opt_level] = challengeParts; let password; try { password = (0, node_crypto_1.createHash)(algo).update(this.password).digest("hex"); } catch (err) { console.error(err); this.emit("error", new TypeError(`Algorithm ${algo} not supported`)); return; } let pwhash = null; // try hash algorithms in the order provided by the server for (const algo of hashes.split(",")) { try { const hash = (0, node_crypto_1.createHash)(algo); pwhash = `{${algo}}` + hash.update(password + salt).digest("hex"); break; } catch (_a) { } } if (pwhash) { let counterResponse = `LIT:${this.username}:${pwhash}:${this.language}:${this.database}:`; if (opt_level && opt_level.startsWith("sql=")) { let level = 0; counterResponse += "FILETRANS:"; try { level = Number(opt_level.substring(4)); } catch (err) { this.emit("error", new TypeError("Invalid handshake options level in server challenge")); return; } // process handshake options const options = []; for (const opt of this.handShakeOptions) { if (opt.level < level) { options.push(`${opt.name}=${Number(opt.value)}`); opt.sent = true; } } if (options) counterResponse += options.join(",") + ":"; } this.send(buffer_1.Buffer.from(counterResponse)) .then(() => this.queue.push(new Response())) .catch((err) => this.emit("error", err)); } else { this.emit("error", new TypeError(`None of the hashes ${hashes} are supported`)); } } /** * Raise exception on server by sending bad packet */ requestAbort() { return new Promise((resolve, reject) => { const header = buffer_1.Buffer.allocUnsafe(2).fill(0); // larger than allowed and not final message header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0); // invalid utf8 and too small const badBody = buffer_1.Buffer.concat([ buffer_1.Buffer.from("ERROR"), buffer_1.Buffer.from([0x80]), ]); const outBuff = buffer_1.Buffer.concat([header, badBody]); this.socket.write(outBuff, (err) => __awaiter(this, void 0, void 0, function* () { if (err) reject(err); resolve(); })); }); } send(buff) { return new Promise((resolve, reject) => { let last = 0; let offset = 0; while (last === 0) { const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE); last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0; const header = buffer_1.Buffer.allocUnsafe(2).fill(0); header.writeUint16LE((seg.length << 1) | last, 0); const outBuff = buffer_1.Buffer.concat([header, seg]); this.socket.write(outBuff, (err) => { if (err) reject(err); if (last) resolve(); }); offset += seg.length; } }); } handleTimeout() { this.emit("error", new Error("Timeout")); } handleSocketError(err) { console.error('Socket error: ', err); this.destroy(); } request(sql, stream = false) { return __awaiter(this, void 0, void 0, function* () { if (this.ready() === false) throw new Error("Not Connected"); yield this.send(buffer_1.Buffer.from(sql)); return new Promise((resolve, reject) => { const resp = new Response({ stream, callbacks: { resolve, reject }, }); this.queue.push(resp); }); }); } requestFileTransfer(buff, fileHandler) { return __awaiter(this, void 0, void 0, function* () { yield this.send(buff); const resp = new Response({ fileHandler }); this.queue.push(resp); }); } requestFileTransferError(err, fileHandler) { return __awaiter(this, void 0, void 0, function* () { yield this.send(buffer_1.Buffer.from(err)); const resp = new Response({ fileHandler }); this.queue.push(resp); }); } recv(data) { let bytesLeftOver; let resp; // process queue left to right, // find 1st uncomplete response // remove complete responses while (this.queue.length) { const next = this.queue[0]; if (next.complete() || next.settled) { this.queue.shift(); } else { resp = next; break; } } if (resp === undefined && this.queue.length === 0) { // challenge message // or direct call to send has being made // request api appends Response to the queue resp = new Response(); this.queue.push(resp); } let offset = 0; let end = data.length; do { offset += resp.append(data.subarray(offset, end)); } while ((offset < end) && !resp.complete()); if (resp.queryStream) resp.streamResult(); if (resp.complete()) this.handleResponse(resp); bytesLeftOver = data.length - offset; if (bytesLeftOver) { //const msg = `some ${bytesLeftOver} bytes left over!`; //console.warn(msg); this.recv(data.subarray(offset)); } } handleResponse(resp) { const err = resp.errorMessage(); if (this.state == MAPI_STATE.CONNECTED) { if (err) { this.emit("error", new Error(err)); return; } if (resp.isRedirect()) { this.redirects += 1; if (this.redirects > MAX_REDIRECTS) this.emit("error", new Error(`Exceeded max number of redirects ${MAX_REDIRECTS}`)); return; } if (resp.isPrompt()) { //console.log("login OK"); this.state = MAPI_STATE.READY; this.emit("ready", this.state); return; } return this.login(resp.toString()); } if (resp.isFileTransfer()) { //console.log("file transfer"); let fhandler; const msg = resp.toString(MSG_FILETRANS.length).trim(); let mode, offset, file; if (msg.startsWith("r ")) { [mode, offset, file] = msg.split(" "); fhandler = resp.fileHandler || new file_transfer_1.FileUploader(this, file, parseInt(offset)); return resp.settle(fhandler.upload()); } else if (msg.startsWith("rb")) { [mode, file] = msg.split(" "); fhandler = resp.fileHandler || new file_transfer_1.FileUploader(this, file, 0); return resp.settle(fhandler.upload()); } else if (msg.startsWith("w")) { [mode, file] = msg.split(" "); fhandler = resp.fileHandler || new file_transfer_1.FileDownloader(this, file); return resp.settle(fhandler.download()); } else { // no msg end of transfer const fileHandler = resp.fileHandler; // we do expect a final response from server this.queue.push(new Response({ fileHandler })); return resp.settle(fileHandler.close()); } } if (resp.isMsgMore()) { // console.log("server wants more"); if (resp.fileHandler instanceof file_transfer_1.FileUploader) return resp.settle(resp.fileHandler.upload()); } if (resp.fileHandler instanceof file_transfer_1.FileDownloader && resp.fileHandler.ready()) { // end of download const fileHandler = resp.fileHandler; const buff = buffer_1.Buffer.concat(resp.chunks); fileHandler.writeChunk(buff); // we do expect a final response from server this.queue.push(new Response({ fileHandler })); return resp.settle(fileHandler.close()); } if (resp.isQueryResponse()) { const data = []; resp.parseQueryResponse(resp.toString(), data); resp.result.data = data; } resp.settle(); } } exports.MapiConnection = MapiConnection; //# sourceMappingURL=mapi.js.map