diff --git a/lib/mysql.js b/lib/mysql.js index b294329..6f67f6c 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -1,47 +1,189 @@ -import {json} from "micro"; -import {createPool} from "mysql"; -import connectionConfig from "mysql/lib/ConnectionConfig.js"; -import types from "mysql/lib/protocol/constants/types.js"; import JSONStream from "JSONStream"; +import {json} from "micro"; +import mysql, {createConnection} from "mysql2"; +import {failedCheck} from "./errors.js"; -const {parseUrl} = connectionConfig; +const {Types, ConnectionConfig} = mysql; -export default (url) => { - const pool = createPool(parseUrl(url)); +export async function query(req, res, pool) { + const {sql, params} = await json(req); + const keepAlive = setInterval(() => res.write("\n"), 25e3); - return async function query(req, res) { - const {sql, params} = await json(req); + let fields; + let rowCount = 0; + let bytes = 0; + try { + await new Promise((resolve, reject) => { + const stream = pool + .query({sql, timeout: 240e3}, params) + .once("fields", (f) => { + res.write(`{"schema":${JSON.stringify(schema((fields = f)))}`); + }) + .stream() + .on("end", resolve) + .on("error", (error) => { + if (!stream.destroyed) stream.destroy(); + reject(error); + }) + .once("readable", () => clearInterval(keepAlive)) + .pipe(JSONStream.stringify(`,"data":[`, ",", "]}")) + .on("data", (chunk) => { + bytes += chunk.length; + rowCount++; + if (rowCount && rowCount % 2e3 === 0) + req.log({ + progress: { + rows: rowCount, + fields: fields.length, + bytes, + done: false, + }, + }); + }); + stream.pipe(res, {end: false}); + }); + } catch (error) { + if (!error.statusCode) error.statusCode = 400; + throw error; + } finally { + clearInterval(keepAlive); + } - let fields; + req.log({ + progress: { + rows: rowCount, + fields: fields ? fields.length : 0, + bytes, + done: true, + }, + }); + + res.end(); +} + +export async function queryStream(req, res, pool) { + const {sql, params} = await json(req); + res.setHeader("Content-Type", "text/plain"); + const keepAlive = setInterval(() => res.write("\n"), 25e3); + + let fields; + let rowCount = 0; + let bytes = 0; + + try { await new Promise((resolve, reject) => { const stream = pool - .query({sql, timeout: 30e3}, params) - .on("fields", (f) => (fields = f)) + .query({sql, timeout: 240e3}, params) + .once("fields", (f) => { + res.write(JSON.stringify(schema((fields = f)))); + res.write("\n"); + }) .stream() .on("end", resolve) .on("error", (error) => { - stream.destroy(); + if (!stream.destroyed) stream.destroy(); reject(error); }) - .pipe(JSONStream.stringify(`{"data":[`, ",", "]")); + .once("readable", () => clearInterval(keepAlive)) + .pipe(JSONStream.stringify("", "\n", "\n")) + .on("data", (chunk) => { + bytes += chunk.length; + rowCount++; + if (rowCount % 2e3 === 0) + req.log({ + progress: { + rows: rowCount, + fields: fields.length, + bytes, + done: false, + }, + }); + }); stream.pipe(res, {end: false}); }); + } catch (error) { + if (!error.statusCode) error.statusCode = 400; + throw error; + } finally { + clearInterval(keepAlive); + } + + req.log({ + progress: { + rows: rowCount, + fields: fields ? fields.length : 0, + bytes, + done: true, + }, + }); + + res.end(); +} + +const READ_ONLY = new Set(["SELECT", "SHOW DATABASES", "SHOW VIEW", "USAGE"]); +export async function check(req, res, pool) { + const rows = await new Promise((resolve, reject) => { + pool.query("SHOW GRANTS FOR CURRENT_USER", (error, results) => { + error ? reject(failedCheck(error.message)) : resolve(results); + }); + }); + const grants = [].concat( + ...rows.map((grant) => + Object.values(grant)[0] + .match(/^GRANT (.+) ON/)[1] + .split(", ") + ) + ); + const permissive = grants.filter((g) => !READ_ONLY.has(g)); + if (permissive.length) + throw failedCheck( + `User has too permissive grants: ${permissive.join(", ")}` + ); + + return {ok: true}; +} - const schema = { - type: "array", - items: { - type: "object", - properties: fields.reduce( - (schema, {name, type, charsetNr}) => ( - (schema[name] = dataTypeSchema({type, charsetNr})), schema - ), - {} +export default (url) => async (req, res) => { + const config = ConnectionConfig.parseUrl(url); + + // Unless specified as a property of the url connection string, ssl is used with the default. + // See https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html#cj-conn-prop_sslMode + if (config.sslMode !== "DISABLED") { + config.ssl = {}; + } + + // the mysql2.createConnection method is not happy if we pass any extra properties not recognized by it. + delete config.sslMode; + + const connection = createConnection({ + ...config, + decimalNumbers: true, + }); + + if (req.method === "POST") { + if (req.url === "/query") return query(req, res, connection); + if (req.url === "/query-stream") return queryStream(req, res, connection); + if (req.url === "/check") return check(req, res, connection); + } + + throw notFound(); +}; + +function schema(fields) { + return { + type: "array", + items: { + type: "object", + properties: fields.reduce( + (schema, {name, type, characterSet}) => ( + (schema[name] = dataTypeSchema({type, charsetNr: characterSet})), + schema ), - }, - }; - res.end(`,"schema":${JSON.stringify(schema)}}`); + {} + ), + }, }; -}; +} // https://github.com/mysqljs/mysql/blob/5569e02ad72789f4b396d9a901f0390fe11b5b4e/lib/protocol/constants/types.js // https://github.com/mysqljs/mysql/blob/5569e02ad72789f4b396d9a901f0390fe11b5b4e/lib/protocol/packets/RowDataPacket.js#L53 @@ -52,46 +194,53 @@ const boolean = ["null", "boolean"], string = ["null", "string"]; function dataTypeSchema({type, charsetNr}) { switch (type) { - case types.BIT: + case Types.BIT: return {type: boolean}; - case types.TINY: - case types.SHORT: - case types.LONG: - return {type: integer}; - case types.INT24: - case types.YEAR: - case types.FLOAT: - case types.DOUBLE: - case types.DECIMAL: - case types.NEWDECIMAL: - return {type: number}; - case types.TIMESTAMP: - case types.DATE: - case types.DATETIME: - case types.NEWDATE: - case types.TIMESTAMP2: - case types.DATETIME2: - case types.TIME2: + case Types.TINY: + return {type: integer, tiny: true}; + case Types.SHORT: + return {type: integer, short: true}; + case Types.LONG: + return {type: integer, long: true}; + case Types.INT24: + return {type: number, int24: true}; + case Types.YEAR: + return {type: number, year: true}; + case Types.FLOAT: + return {type: number, float: true}; + case Types.DOUBLE: + return {type: number, double: true}; + case Types.DECIMAL: + return {type: number, decimal: true}; + case Types.NEWDECIMAL: + return {type: number, newdecimal: true}; + case Types.TIMESTAMP: + case Types.DATE: + case Types.DATETIME: + case Types.NEWDATE: + case Types.TIMESTAMP2: + case Types.DATETIME2: + case Types.TIME2: return {type: string, date: true}; - case types.LONGLONG: // TODO + case Types.LONGLONG: return {type: string, bigint: true}; - case types.TINY_BLOB: - case types.MEDIUM_BLOB: - case types.LONG_BLOB: - case types.BLOB: - case types.VAR_STRING: - case types.VARCHAR: - case types.STRING: + case Types.TINY_BLOB: + case Types.MEDIUM_BLOB: + case Types.LONG_BLOB: + case Types.BLOB: + case Types.VAR_STRING: + case Types.VARCHAR: + case Types.STRING: return charsetNr === 63 // binary ? {type: object, buffer: true} : {type: string}; - case types.JSON: - return {type: object}; - case types.TIME: // TODO - case types.ENUM: // TODO - case types.SET: // TODO - case types.GEOMETRY: // TODO - case types.NULL: // TODO + case Types.JSON: + return {type: object, json: true}; + case Types.TIME: // TODO + case Types.ENUM: // TODO + case Types.SET: // TODO + case Types.GEOMETRY: // TODO + case Types.NULL: // TODO default: return {type: string}; } diff --git a/lib/server.js b/lib/server.js index e79551f..075437e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -6,7 +6,7 @@ import https from "https"; import {readFileSync} from "fs"; import {run} from "micro"; import {createHmac, timingSafeEqual} from "crypto"; -import serializeErrors from "./serialize-errors.js"; +import serializeErrors from "../middleware/serialize-errors.js"; import {notFound, unauthorized, exit} from "./errors.js"; import mysql from "./mysql.js"; import postgres from "./postgres.js"; @@ -14,6 +14,7 @@ import snowflake from "./snowflake.js"; import mssql from "./mssql.js"; import oracle from "./oracle.js"; import databricks from "./databricks.js"; +import logger from "../middleware/logger.js"; export async function server(config, argv) { const development = process.env.NODE_ENV === "development"; @@ -34,7 +35,7 @@ export async function server(config, argv) { } = config; const handler = - type === "mysql" + type === "mysql" || type === "mongosql" ? mysql(url) : type === "postgres" ? postgres(url) @@ -63,11 +64,11 @@ export async function server(config, argv) { const sslcert = readFileSync(argv.sslcert); const sslkey = readFileSync(argv.sslkey); server = https.createServer({cert: sslcert, key: sslkey}, (req, res) => - run(req, res, serializeErrors(index)) + run(req, res, logger(serializeErrors(index))) ); } else { server = http.createServer((req, res) => - run(req, res, serializeErrors(index)) + run(req, res, logger(serializeErrors(index))) ); } diff --git a/middleware/logger.js b/middleware/logger.js new file mode 100644 index 0000000..6d86258 --- /dev/null +++ b/middleware/logger.js @@ -0,0 +1,22 @@ +// Recursively flatten key names separated by dots. +function* entries(data, prefix = []) { + if ( + data instanceof Object && + Object.getPrototypeOf(data) === Object.prototype + ) { + for (const [key, value] of Object.entries(data)) + yield* entries(value, prefix.concat(key)); + } else { + yield [prefix.join("."), data]; + } +} + +export default (handler) => (req, res) => { + req.log = function log(data) { + const requestId = req.headers["x-request-id"]; + const parts = requestId ? [`http.request_id=${requestId}`] : []; + for (const [key, value] of entries(data)) parts.push(`${key}=${value}`); + console.log(parts.join(" ")); + }; + return handler(req, res); +}; diff --git a/lib/serialize-errors.js b/middleware/serialize-errors.js similarity index 100% rename from lib/serialize-errors.js rename to middleware/serialize-errors.js diff --git a/package.json b/package.json index 77d1dab..cc0dfe6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "ajv": "^8.11.0", "micro": "^9.3.4", "mssql": "^9.0.1", - "mysql": "^2.17.1", + "mysql2": "^3.0.1", "open": "^6.3.0", "pg": "^8.7.1", "pg-query-stream": "^4.2.1", diff --git a/yarn.lock b/yarn.lock index 2a49749..6c13730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1547,11 +1547,6 @@ big-integer@^1.6.43: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== -bignumber.js@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" - integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== - bignumber.js@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8" @@ -2138,6 +2133,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -2574,6 +2574,13 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + generic-pool@^3.8.2: version "3.9.0" resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" @@ -3166,6 +3173,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" @@ -3535,6 +3547,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f" + integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A== + loupe@^2.3.1: version "2.3.4" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" @@ -3562,6 +3579,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^7.14.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" + integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -3762,15 +3784,19 @@ mssql@^9.0.1: tarn "^3.0.2" tedious "^15.0.1" -mysql@^2.17.1: - version "2.18.1" - resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" - integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== +mysql2@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.0.1.tgz#436db56e96d5b7fed350192387f54881658b8b44" + integrity sha512-Wrh5KuE0OOlm6wRwRhE2q+C8LjwwfT3sFKVauyTwMwPbOd2i0SzxMqTZPqs90ZNAEWjot5GFywje84qVn3ITYw== dependencies: - bignumber.js "9.0.0" - readable-stream "2.3.7" - safe-buffer "5.1.2" - sqlstring "2.3.1" + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.6.3" + long "^5.2.1" + lru-cache "^7.14.1" + named-placeholders "^1.1.3" + seq-queue "^0.0.5" + sqlstring "^2.3.2" mz@^2.7.0: version "2.7.0" @@ -3781,6 +3807,13 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.1" + nan@^2.12.1: version "2.17.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" @@ -4399,7 +4432,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@2.3.7, readable-stream@^2.0.2: +readable-stream@^2.0.2: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -4590,16 +4623,16 @@ rxjs@^7.5.4: dependencies: tslib "^2.1.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -4653,6 +4686,11 @@ semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + serialize-error@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-4.1.0.tgz#63e1e33ede20bcd89d9f0528ea4c15fbf0f2b78a" @@ -4877,10 +4915,10 @@ sprintf-js@^1.1.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== -sqlstring@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" - integrity sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ== +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== stack-trace@0.0.x: version "0.0.10"