|
| 1 | +"use strict"; |
| 2 | +/** |
| 3 | + * connection-tls.ts - connection via tls socket and sqlitecloud protocol |
| 4 | + */ |
| 5 | +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { |
| 6 | + if (k2 === undefined) k2 = k; |
| 7 | + var desc = Object.getOwnPropertyDescriptor(m, k); |
| 8 | + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { |
| 9 | + desc = { enumerable: true, get: function() { return m[k]; } }; |
| 10 | + } |
| 11 | + Object.defineProperty(o, k2, desc); |
| 12 | +}) : (function(o, m, k, k2) { |
| 13 | + if (k2 === undefined) k2 = k; |
| 14 | + o[k2] = m[k]; |
| 15 | +})); |
| 16 | +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { |
| 17 | + Object.defineProperty(o, "default", { enumerable: true, value: v }); |
| 18 | +}) : function(o, v) { |
| 19 | + o["default"] = v; |
| 20 | +}); |
| 21 | +var __importStar = (this && this.__importStar) || (function () { |
| 22 | + var ownKeys = function(o) { |
| 23 | + ownKeys = Object.getOwnPropertyNames || function (o) { |
| 24 | + var ar = []; |
| 25 | + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; |
| 26 | + return ar; |
| 27 | + }; |
| 28 | + return ownKeys(o); |
| 29 | + }; |
| 30 | + return function (mod) { |
| 31 | + if (mod && mod.__esModule) return mod; |
| 32 | + var result = {}; |
| 33 | + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); |
| 34 | + __setModuleDefault(result, mod); |
| 35 | + return result; |
| 36 | + }; |
| 37 | +})(); |
| 38 | +Object.defineProperty(exports, "__esModule", { value: true }); |
| 39 | +exports.SQLiteCloudTlsConnection = void 0; |
| 40 | +const connection_1 = require("./connection"); |
| 41 | +const protocol_1 = require("./protocol"); |
| 42 | +const types_1 = require("./types"); |
| 43 | +const utilities_1 = require("./utilities"); |
| 44 | +// explicitly importing buffer library to allow cross-platform support by replacing it |
| 45 | +const buffer_1 = require("buffer"); |
| 46 | +const tls = __importStar(require("tls")); |
| 47 | +/** |
| 48 | + * Implementation of SQLiteCloudConnection that connects to the database using specific tls APIs |
| 49 | + * that connect to native sockets or tls sockets and communicates via raw, binary protocol. |
| 50 | + */ |
| 51 | +class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection { |
| 52 | + constructor() { |
| 53 | + super(...arguments); |
| 54 | + // processCommands sets up empty buffers, results callback then send the command to the server via socket.write |
| 55 | + // onData is called when data is received, it will process the data until all data is retrieved for a response |
| 56 | + // when response is complete or there's an error, finish is called to call the results callback set by processCommands... |
| 57 | + // buffer to accumulate incoming data until an whole command is received and can be parsed |
| 58 | + this.buffer = buffer_1.Buffer.alloc(0); |
| 59 | + this.startedOn = new Date(); |
| 60 | + this.pendingChunks = []; |
| 61 | + } |
| 62 | + /** True if connection is open */ |
| 63 | + get connected() { |
| 64 | + return !!this.socket; |
| 65 | + } |
| 66 | + /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */ |
| 67 | + connectTransport(config, callback) { |
| 68 | + console.assert(!this.connected, 'SQLiteCloudTlsConnection.connect - connection already established'); |
| 69 | + if (this.config.verbose) { |
| 70 | + console.debug(`-> connecting ${config === null || config === void 0 ? void 0 : config.host}:${config === null || config === void 0 ? void 0 : config.port}`); |
| 71 | + } |
| 72 | + this.config = config; |
| 73 | + const initializationCommands = (0, utilities_1.getInitializationCommands)(config); |
| 74 | + // connect to plain socket, without encryption, only if insecure parameter specified |
| 75 | + // this option is mainly for testing purposes and is not available on production nodes |
| 76 | + // which would need to connect using tls and proper certificates as per code below |
| 77 | + const connectionOptions = { |
| 78 | + host: config.host, |
| 79 | + port: config.port, |
| 80 | + rejectUnauthorized: config.host != 'localhost', |
| 81 | + // Server name for the SNI (Server Name Indication) TLS extension. |
| 82 | + // https://r2.nodejs.org/docs/v6.11.4/api/tls.html#tls_class_tls_tlssocket |
| 83 | + servername: config.host |
| 84 | + }; |
| 85 | + // tls.connect in the react-native-tcp-socket library is tls.connectTLS |
| 86 | + let connector = tls.connect; |
| 87 | + // @ts-ignore |
| 88 | + if (typeof tls.connectTLS !== 'undefined') { |
| 89 | + // @ts-ignore |
| 90 | + connector = tls.connectTLS; |
| 91 | + } |
| 92 | + this.socket = connector(connectionOptions, () => { |
| 93 | + var _a; |
| 94 | + if (this.config.verbose) { |
| 95 | + console.debug(`SQLiteCloudTlsConnection - connected to ${this.config.host}, authorized: ${(_a = this.socket) === null || _a === void 0 ? void 0 : _a.authorized}`); |
| 96 | + } |
| 97 | + this.transportCommands(initializationCommands, error => { |
| 98 | + if (this.config.verbose) { |
| 99 | + console.debug(`SQLiteCloudTlsConnection - initialized connection`); |
| 100 | + } |
| 101 | + callback === null || callback === void 0 ? void 0 : callback.call(this, error); |
| 102 | + }); |
| 103 | + }); |
| 104 | + this.socket.setKeepAlive(true); |
| 105 | + // disable Nagle algorithm because we want our writes to be sent ASAP |
| 106 | + // https://brooker.co.za/blog/2024/05/09/nagle.html |
| 107 | + this.socket.setNoDelay(true); |
| 108 | + this.socket.on('data', data => { |
| 109 | + this.processCommandsData(data); |
| 110 | + }); |
| 111 | + this.socket.on('error', error => { |
| 112 | + this.close(); |
| 113 | + this.processCommandsFinish(new types_1.SQLiteCloudError('Connection error', { errorCode: 'ERR_CONNECTION_ERROR', cause: error })); |
| 114 | + }); |
| 115 | + this.socket.on('end', () => { |
| 116 | + this.close(); |
| 117 | + if (this.processCallback) |
| 118 | + this.processCommandsFinish(new types_1.SQLiteCloudError('Server ended the connection', { errorCode: 'ERR_CONNECTION_ENDED' })); |
| 119 | + }); |
| 120 | + this.socket.on('close', () => { |
| 121 | + this.close(); |
| 122 | + this.processCommandsFinish(new types_1.SQLiteCloudError('Connection closed', { errorCode: 'ERR_CONNECTION_CLOSED' })); |
| 123 | + }); |
| 124 | + this.socket.on('timeout', () => { |
| 125 | + this.close(); |
| 126 | + this.processCommandsFinish(new types_1.SQLiteCloudError('Connection ened due to timeout', { errorCode: 'ERR_CONNECTION_TIMEOUT' })); |
| 127 | + }); |
| 128 | + return this; |
| 129 | + } |
| 130 | + /** Will send a command immediately (no queueing), return the rowset/result or throw an error */ |
| 131 | + transportCommands(commands, callback) { |
| 132 | + var _a, _b, _c, _d, _e; |
| 133 | + // connection needs to be established? |
| 134 | + if (!this.socket) { |
| 135 | + callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' })); |
| 136 | + return this; |
| 137 | + } |
| 138 | + if (typeof commands === 'string') { |
| 139 | + commands = { query: commands }; |
| 140 | + } |
| 141 | + // reset buffer and rowset chunks, define response callback |
| 142 | + this.buffer = buffer_1.Buffer.alloc(0); |
| 143 | + this.startedOn = new Date(); |
| 144 | + this.processCallback = callback; |
| 145 | + this.executingCommands = commands; |
| 146 | + // compose commands following SCPC protocol |
| 147 | + const formattedCommands = (0, protocol_1.formatCommand)(commands); |
| 148 | + if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.verbose) { |
| 149 | + console.debug(`-> ${formattedCommands}`); |
| 150 | + } |
| 151 | + const timeoutMs = (_c = (_b = this.config) === null || _b === void 0 ? void 0 : _b.timeout) !== null && _c !== void 0 ? _c : 0; |
| 152 | + if (timeoutMs > 0) { |
| 153 | + const timeout = setTimeout(() => { |
| 154 | + var _a; |
| 155 | + callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection timeout out', { errorCode: 'ERR_CONNECTION_TIMEOUT' })); |
| 156 | + (_a = this.socket) === null || _a === void 0 ? void 0 : _a.destroy(); |
| 157 | + this.socket = undefined; |
| 158 | + }, timeoutMs); |
| 159 | + (_d = this.socket) === null || _d === void 0 ? void 0 : _d.write(formattedCommands, () => { |
| 160 | + clearTimeout(timeout); // Clear the timeout on successful write |
| 161 | + }); |
| 162 | + } |
| 163 | + else { |
| 164 | + (_e = this.socket) === null || _e === void 0 ? void 0 : _e.write(formattedCommands); |
| 165 | + } |
| 166 | + return this; |
| 167 | + } |
| 168 | + /** Handles data received in response to an outbound command sent by processCommands */ |
| 169 | + processCommandsData(data) { |
| 170 | + var _a, _b, _c, _d, _e, _f, _g; |
| 171 | + try { |
| 172 | + // append data to buffer as it arrives |
| 173 | + if (data.length && data.length > 0) { |
| 174 | + // console.debug(`processCommandsData - received ${data.length} bytes`) |
| 175 | + this.buffer = buffer_1.Buffer.concat([this.buffer, data]); |
| 176 | + } |
| 177 | + let dataType = (_a = this.buffer) === null || _a === void 0 ? void 0 : _a.subarray(0, 1).toString(); |
| 178 | + if ((0, protocol_1.hasCommandLength)(dataType)) { |
| 179 | + const commandLength = (0, protocol_1.parseCommandLength)(this.buffer); |
| 180 | + const hasReceivedEntireCommand = this.buffer.length - this.buffer.indexOf(' ') - 1 >= commandLength ? true : false; |
| 181 | + if (hasReceivedEntireCommand) { |
| 182 | + if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.verbose) { |
| 183 | + let bufferString = this.buffer.toString('utf8'); |
| 184 | + if (bufferString.length > 1000) { |
| 185 | + bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40); |
| 186 | + } |
| 187 | + const elapsedMs = new Date().getTime() - this.startedOn.getTime(); |
| 188 | + console.debug(`<- ${bufferString} (${bufferString.length} bytes, ${elapsedMs}ms)`); |
| 189 | + } |
| 190 | + // need to decompress this buffer before decoding? |
| 191 | + if (dataType === protocol_1.CMD_COMPRESSED) { |
| 192 | + const decompressResults = (0, protocol_1.decompressBuffer)(this.buffer); |
| 193 | + if (decompressResults.dataType === protocol_1.CMD_ROWSET_CHUNK) { |
| 194 | + this.pendingChunks.push(decompressResults.buffer); |
| 195 | + this.buffer = decompressResults.remainingBuffer; |
| 196 | + this.processCommandsData(buffer_1.Buffer.alloc(0)); |
| 197 | + return; |
| 198 | + } |
| 199 | + else { |
| 200 | + const { data } = (0, protocol_1.popData)(decompressResults.buffer); |
| 201 | + (_c = this.processCommandsFinish) === null || _c === void 0 ? void 0 : _c.call(this, null, data); |
| 202 | + } |
| 203 | + } |
| 204 | + else { |
| 205 | + if (dataType !== protocol_1.CMD_ROWSET_CHUNK) { |
| 206 | + const { data } = (0, protocol_1.popData)(this.buffer); |
| 207 | + (_d = this.processCommandsFinish) === null || _d === void 0 ? void 0 : _d.call(this, null, data); |
| 208 | + } |
| 209 | + else { |
| 210 | + const completeChunk = (0, protocol_1.bufferEndsWith)(this.buffer, protocol_1.ROWSET_CHUNKS_END); |
| 211 | + if (completeChunk) { |
| 212 | + const parsedData = (0, protocol_1.parseRowsetChunks)([...this.pendingChunks, this.buffer]); |
| 213 | + (_e = this.processCommandsFinish) === null || _e === void 0 ? void 0 : _e.call(this, null, parsedData); |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | + else { |
| 220 | + // command with no explicit len so make sure that the final character is a space |
| 221 | + const lastChar = this.buffer.subarray(this.buffer.length - 1, this.buffer.length).toString('utf8'); |
| 222 | + if (lastChar == ' ') { |
| 223 | + const { data } = (0, protocol_1.popData)(this.buffer); |
| 224 | + (_f = this.processCommandsFinish) === null || _f === void 0 ? void 0 : _f.call(this, null, data); |
| 225 | + } |
| 226 | + } |
| 227 | + } |
| 228 | + catch (error) { |
| 229 | + console.error(`processCommandsData - error: ${error}`); |
| 230 | + console.assert(error instanceof Error, 'An error occoured while processing data'); |
| 231 | + if (error instanceof Error) { |
| 232 | + (_g = this.processCommandsFinish) === null || _g === void 0 ? void 0 : _g.call(this, error); |
| 233 | + } |
| 234 | + } |
| 235 | + } |
| 236 | + /** Completes a transaction initiated by processCommands */ |
| 237 | + processCommandsFinish(error, result) { |
| 238 | + if (error) { |
| 239 | + if (this.processCallback) { |
| 240 | + console.error('processCommandsFinish - error', error); |
| 241 | + } |
| 242 | + else { |
| 243 | + console.warn('processCommandsFinish - error with no registered callback', error); |
| 244 | + } |
| 245 | + } |
| 246 | + if (this.processCallback) { |
| 247 | + this.processCallback(error, result); |
| 248 | + } |
| 249 | + this.buffer = buffer_1.Buffer.alloc(0); |
| 250 | + this.pendingChunks = []; |
| 251 | + } |
| 252 | + /** Disconnect immediately, release connection, no events. */ |
| 253 | + close() { |
| 254 | + if (this.socket) { |
| 255 | + this.socket.removeAllListeners(); |
| 256 | + this.socket.destroy(); |
| 257 | + this.socket = undefined; |
| 258 | + } |
| 259 | + this.operations.clear(); |
| 260 | + return this; |
| 261 | + } |
| 262 | +} |
| 263 | +exports.SQLiteCloudTlsConnection = SQLiteCloudTlsConnection; |
| 264 | +exports.default = SQLiteCloudTlsConnection; |
0 commit comments