From 22cb90b6f6a444a2615b8f48d1d85753b3dc2734 Mon Sep 17 00:00:00 2001 From: Fabian Stamm Date: Wed, 28 Oct 2020 01:00:39 +0100 Subject: [PATCH] Switching to new security rules --- .env | 2 +- Dockerfile | 6 +- package-lock.json | 214 +++++++++++++++--------- package.json | 21 ++- src/config.ts | 11 +- src/database/database.ts | 55 ++++-- src/database/query.ts | 24 +-- src/database/rules.ts | 125 -------------- src/rules/compile.ts | 296 +++++++++++++++++++++++++++++++++ src/rules/error.ts | 36 ++++ src/rules/index.ts | 30 ++++ src/rules/parser.ts | 349 +++++++++++++++++++++++++++++++++++++++ src/rules/tokenise.ts | 99 +++++++++++ src/web/helper/form.ts | 10 +- src/web/v1/admin.ts | 46 +++--- tsconfig.json | 36 ++-- views/admin.html | 17 +- views/forms.hbs | 18 ++ 18 files changed, 1094 insertions(+), 301 deletions(-) delete mode 100644 src/database/rules.ts create mode 100644 src/rules/compile.ts create mode 100644 src/rules/error.ts create mode 100644 src/rules/index.ts create mode 100644 src/rules/parser.ts create mode 100644 src/rules/tokenise.ts diff --git a/.env b/.env index 7440cf5..08aa9ef 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ PORT = 5000 ADMIN_KEY = test ACCESS_LOG = true -DEV = true \ No newline at end of file +DEV = true diff --git a/Dockerfile b/Dockerfile index 50c338f..e4833e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 +FROM node:14 LABEL maintainer="Fabian Stamm " @@ -8,6 +8,8 @@ LABEL maintainer="Fabian Stamm " RUN mkdir -p /usr/src/app WORKDIR /usr/src/app +RUN npm config set registry https://npm.hibas123.de/ + ENV NODE_ENV=production COPY ["package.json", "package-lock.json", "/usr/src/app/"] @@ -20,4 +22,4 @@ VOLUME [ "/usr/src/app/databases", "/usr/src/app/logs" ] EXPOSE 5000/tcp -CMD ["npm", "run", "start"] \ No newline at end of file +CMD ["npm", "run", "start"] diff --git a/package-lock.json b/package-lock.json index 9bf52d2..570c7f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,9 +22,9 @@ } }, "@hibas123/utils": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@hibas123/utils/-/utils-2.2.4.tgz", - "integrity": "sha512-Mx9H73Q2PNLzqRp3sHN7esskhUJ6pNFHht6u0ykYeHt6jb/kdsISCQPvIHWC32cvAWylBFQ2qHF1dEFEcZyL+Q==" + "version": "2.2.16", + "resolved": "https://npm.hibas123.de/@hibas123%2futils/-/utils-2.2.16.tgz", + "integrity": "sha512-nwzJL+vaWpbaGmIc+AU6T/7ZZ+ZiB57NOEECIu3GAVB86qo21o12gr0a4AYv/bGzWKupxuT5a3fDyrRQybKLWw==" }, "@sindresorhus/is": { "version": "0.14.0", @@ -66,12 +66,6 @@ "@types/node": "*" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, "@types/connect": { "version": "3.4.33", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", @@ -114,9 +108,9 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" }, "@types/express": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.7.tgz", - "integrity": "sha512-dCOT5lcmV/uC2J9k0rPafATeeyz+99xTt54ReX11/LObZgfzJqZNcW27zGhYyX+9iSEGXGt5qLPwRSvBZcLvtQ==", + "version": "4.17.8", + "resolved": "https://npm.hibas123.de/@types%2fexpress/-/express-4.17.8.tgz", + "integrity": "sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==", "dev": true, "requires": { "@types/body-parser": "*", @@ -126,9 +120,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.9", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz", - "integrity": "sha512-DG0BYg6yO+ePW+XoDENYz8zhNGC3jDDEpComMYn7WJc4mY1Us8Rw9ax2YhJXxpyk2SF47PQAoQ0YyVT1a0bEkA==", + "version": "4.17.13", + "resolved": "https://npm.hibas123.de/@types%2fexpress-serve-static-core/-/express-serve-static-core-4.17.13.tgz", + "integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==", "dev": true, "requires": { "@types/node": "*", @@ -151,6 +145,12 @@ "integrity": "sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ==", "dev": true }, + "@types/http-errors": { + "version": "1.8.0", + "resolved": "https://npm.hibas123.de/@types%2fhttp-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==", + "dev": true + }, "@types/jsonwebtoken": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", @@ -167,15 +167,16 @@ "dev": true }, "@types/koa": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.11.3.tgz", - "integrity": "sha512-ABxVkrNWa4O/Jp24EYI/hRNqEVRlhB9g09p48neQp4m3xL1TJtdWk2NyNQSMCU45ejeELMQZBYyfstyVvO2H3Q==", + "version": "2.11.6", + "resolved": "https://npm.hibas123.de/@types%2fkoa/-/koa-2.11.6.tgz", + "integrity": "sha512-BhyrMj06eQkk04C97fovEDQMpLpd2IxCB4ecitaXwOKGq78Wi2tooaDOWOFGajPk8IkQOAtMppApgSVkYe1F/A==", "dev": true, "requires": { "@types/accepts": "*", "@types/content-disposition": "*", "@types/cookies": "*", "@types/http-assert": "*", + "@types/http-errors": "*", "@types/keygrip": "*", "@types/koa-compose": "*", "@types/node": "*" @@ -235,14 +236,14 @@ } }, "@types/node": { - "version": "14.0.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", - "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==" + "version": "14.14.5", + "resolved": "https://npm.hibas123.de/@types%2fnode/-/node-14.14.5.tgz", + "integrity": "sha512-H5Wn24s/ZOukBmDn03nnGTp18A60ny9AmCwnEcgJiTgSGsCO7k+NWP7zjCCbhlcnVCoI+co52dUAt9GMhOSULw==" }, "@types/qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==", + "version": "6.9.5", + "resolved": "https://npm.hibas123.de/@types%2fqs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==", "dev": true }, "@types/range-parser": { @@ -252,19 +253,19 @@ "dev": true }, "@types/serve-static": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.5.tgz", - "integrity": "sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ==", + "version": "1.13.6", + "resolved": "https://npm.hibas123.de/@types%2fserve-static/-/serve-static-1.13.6.tgz", + "integrity": "sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA==", "dev": true, "requires": { - "@types/express-serve-static-core": "*", - "@types/mime": "*" + "@types/mime": "*", + "@types/node": "*" } }, "@types/ws": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz", - "integrity": "sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==", + "version": "7.2.8", + "resolved": "https://npm.hibas123.de/@types%2fws/-/ws-7.2.8.tgz", + "integrity": "sha512-LGtjDQxcMk4uU7ET85qJWYLwCdsSxLRxqOums/SDDWJw/BCCgFrOvqcvly6rGNkB/OkOiUaRzzhz8pchuxXr7w==", "dev": true, "requires": { "@types/node": "*" @@ -347,6 +348,12 @@ "picomatch": "^2.0.4" } }, + "arg": { + "version": "4.1.3", + "resolved": "https://npm.hibas123.de/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -387,12 +394,11 @@ "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://npm.hibas123.de/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -460,9 +466,9 @@ } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://npm.hibas123.de/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -503,6 +509,12 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://npm.hibas123.de/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -578,9 +590,9 @@ } }, "chokidar": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "version": "3.4.3", + "resolved": "https://npm.hibas123.de/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", "dev": true, "requires": { "anymatch": "~3.1.1", @@ -590,7 +602,7 @@ "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" + "readdirp": "~3.5.0" } }, "ci-info": { @@ -600,9 +612,9 @@ "dev": true }, "cli-boxes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==", + "version": "2.2.1", + "resolved": "https://npm.hibas123.de/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", "dev": true }, "cliui": { @@ -807,10 +819,16 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, + "diff": { + "version": "4.0.2", + "resolved": "https://npm.hibas123.de/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://npm.hibas123.de/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, "requires": { "is-obj": "^2.0.0" @@ -924,7 +942,7 @@ }, "fsevents": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "resolved": "https://npm.hibas123.de/fsevents/-/fsevents-2.1.3.tgz", "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", "dev": true, "optional": true @@ -1460,7 +1478,7 @@ }, "lodash": { "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "resolved": "https://npm.hibas123.de/lodash/-/lodash-4.17.20.tgz", "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, @@ -1522,6 +1540,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://npm.hibas123.de/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1571,9 +1595,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nanoid": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", - "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==" + "version": "3.1.16", + "resolved": "https://npm.hibas123.de/nanoid/-/nanoid-3.1.16.tgz", + "integrity": "sha512-+AK8MN0WHji40lj8AEuwLOvLSbWYApQpre/aFJZD71r43wVRLrOYS4FmJOPQYon1TqB462RzrrxlfA74XRES8w==" }, "napi-macros": { "version": "2.0.0", @@ -1596,9 +1620,9 @@ "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==" }, "nodemon": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", - "integrity": "sha512-Ltced+hIfTmaS28Zjv1BM552oQ3dbwPqI4+zI0SLgq+wpJhSyqgYude/aZa/3i31VCQWMfXJVxvu86abcam3uQ==", + "version": "2.0.6", + "resolved": "https://npm.hibas123.de/nodemon/-/nodemon-2.0.6.tgz", + "integrity": "sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ==", "dev": true, "requires": { "chokidar": "^3.2.2", @@ -1609,8 +1633,8 @@ "semver": "^5.7.1", "supports-color": "^5.5.0", "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^4.0.0" + "undefsafe": "^2.0.3", + "update-notifier": "^4.1.0" }, "dependencies": { "supports-color": { @@ -1806,9 +1830,9 @@ } }, "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "version": "2.1.1", + "resolved": "https://npm.hibas123.de/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", "dev": true, "requires": { "escape-goat": "^2.0.0" @@ -1878,9 +1902,9 @@ } }, "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "version": "3.5.0", + "resolved": "https://npm.hibas123.de/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "dev": true, "requires": { "picomatch": "^2.2.1" @@ -1997,6 +2021,16 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://npm.hibas123.de/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "spawn-command": { "version": "0.0.2-1", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", @@ -2091,9 +2125,9 @@ } }, "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", + "version": "2.2.1", + "resolved": "https://npm.hibas123.de/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true }, "to-readable-stream": { @@ -2131,6 +2165,19 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, + "ts-node": { + "version": "9.0.0", + "resolved": "https://npm.hibas123.de/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", @@ -2167,9 +2214,9 @@ } }, "typescript": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", - "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "version": "4.0.5", + "resolved": "https://npm.hibas123.de/typescript/-/typescript-4.0.5.tgz", + "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", "dev": true }, "uglify-js": { @@ -2213,9 +2260,9 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "update-notifier": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", + "version": "4.1.3", + "resolved": "https://npm.hibas123.de/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", "dev": true, "requires": { "boxen": "^4.2.0", @@ -2234,12 +2281,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://npm.hibas123.de/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -2275,9 +2321,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://npm.hibas123.de/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -2466,6 +2512,12 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://npm.hibas123.de/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true } } } diff --git a/package.json b/package.json index 8b8d95a..0db5390 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,13 @@ { "name": "@hibas123/realtimedb", - "version": "2.0.0-beta.20", + "version": "2.0.0-beta.21", "description": "", "main": "lib/index.js", "private": true, "scripts": { "start": "node lib/index.js", "build": "tsc", - "watch-ts": "tsc -w", - "watch-node": "nodemon --ignore *.ts lib/index.js", - "watch": "concurrently \"npm:watch-*\"", + "watch": "nodemon -e ts --exec ts-node src/index.ts", "build-docker": "npm run build && docker build -t realtimedb .", "prepublishOnly": "tsc" }, @@ -18,20 +16,21 @@ "devDependencies": { "@types/dotenv": "^8.2.0", "@types/jsonwebtoken": "^8.5.0", - "@types/koa": "^2.11.3", + "@types/koa": "^2.11.6", "@types/koa-router": "^7.4.1", "@types/leveldown": "^4.0.2", "@types/levelup": "^4.3.0", "@types/nanoid": "^2.1.0", - "@types/node": "^14.0.27", - "@types/ws": "^7.2.6", + "@types/node": "^14.14.5", + "@types/ws": "^7.2.8", "concurrently": "^5.3.0", - "nodemon": "^2.0.4", - "typescript": "^3.9.7" + "nodemon": "^2.0.6", + "ts-node": "^9.0.0", + "typescript": "^4.0.5" }, "dependencies": { "@hibas123/nodelogging": "^2.4.5", - "@hibas123/utils": "^2.2.4", + "@hibas123/utils": "^2.2.16", "dotenv": "^8.2.0", "handlebars": "^4.7.6", "jsonwebtoken": "^8.5.1", @@ -40,7 +39,7 @@ "koa-router": "^9.4.0", "leveldown": "^5.6.0", "levelup": "^4.4.0", - "nanoid": "^3.1.12", + "nanoid": "^3.1.16", "what-the-pack": "^2.0.3", "ws": "^7.3.1" } diff --git a/src/config.ts b/src/config.ts index 602734b..d5add16 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,25 +1,24 @@ import Logging from "@hibas123/nodelogging"; import * as dotenv from "dotenv"; import { LoggingTypes } from "@hibas123/logging"; -dotenv.config() - +dotenv.config(); interface IConfig { port: number; admin: string; access_log: boolean; - dev: boolean + dev: boolean; } const config: IConfig = { port: Number(process.env.PORT), access_log: (process.env.ACCESS_LOG || "").toLowerCase() === "true", admin: process.env.ADMIN_KEY, - dev: (process.env.DEV || "").toLowerCase() === "true" -} + dev: (process.env.DEV || "").toLowerCase() === "true", +}; if (config.dev) { Logging.logLevel = LoggingTypes.Log; } -export default config; \ No newline at end of file +export default config; diff --git a/src/database/database.ts b/src/database/database.ts index 67d6ee3..bf3e7a3 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,6 +1,5 @@ -import { Rules } from "./rules"; import Settings from "../settings"; -import getLevelDB, { LevelDB, deleteLevelDB, resNull } from "../storage"; +import getLevelDB, { deleteLevelDB, resNull } from "../storage"; import DocumentLock from "./lock"; import { DocumentQuery, @@ -14,6 +13,9 @@ import Logging from "@hibas123/nodelogging"; import Session from "./session"; import nanoid = require("nanoid"); import { Observable } from "@hibas123/utils"; +import { RuleRunner } from "../rules/compile"; +import compileRule from "../rules"; +import { RuleError } from "../rules/error"; const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; @@ -81,17 +83,27 @@ export class Database { return `${collectionid || ""}/${documentid || ""}`; } - private level = getLevelDB(this.name); + #level = getLevelDB(this.name); get data() { - return this.level.data; + return this.#level.data; } get collections() { - return this.level.collection; + return this.#level.collection; + } + + #rules: RuleRunner; + #rawRules?: string; + + get rawRules() { + return this.#rawRules; + } + + get rules() { + return this.#rules; } - public rules: Rules; private locks = new DocumentLock(); public collectionLocks = new DocumentLock(); @@ -107,7 +119,7 @@ export class Database { name: this.name, accesskey: this.accesskey, publickey: this.publickey, - rules: this.rules, + rules: this.#rules, }; } @@ -118,13 +130,36 @@ export class Database { public publickey?: string, public rootkey?: string ) { - if (rawRules) this.rules = new Rules(rawRules); + if (rawRules) this.applyRules(rawRules); + } + + private applyRules(rawRules: string): undefined | RuleError { + try { + JSON.parse(rawRules); + Logging.warning( + "Found old rule! Replacing with a 100% permissive one!" + ); + rawRules = + "service realtimedb {\n match /* {\n allow read, write, list: if false; \n }\n}"; + // still json, so switching to + } catch (err) {} + + let { runner, error } = compileRule(rawRules); + if (error) { + Logging.warning("Found error in existing config!", error); + runner = compileRule("service realtimesb {}").runner; + } + this.#rules = runner; + this.#rawRules = rawRules; + return undefined; } async setRules(rawRules: string) { - let rules = new Rules(rawRules); + const { runner, error } = compileRule(rawRules); + if (error) return error; await Settings.setDatabaseRules(this.name, rawRules); - this.rules = rules; + this.#rules = runner; + this.#rawRules = rawRules; } async setAccessKey(key: string) { diff --git a/src/database/query.ts b/src/database/query.ts index c947fef..4f4e539 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -5,6 +5,7 @@ import Logging from "@hibas123/nodelogging"; import * as MSGPack from "what-the-pack"; import Session from "./session"; import { LevelUpChain } from "levelup"; +import { Operations } from "../rules/parser"; export type IWriteQueries = "set" | "update" | "delete" | "add"; export type ICollectionQueries = @@ -47,7 +48,7 @@ interface IPreparedQuery { needDocument: boolean; batchCompatible: boolean; runner: Runner; - permission: "write" | "read"; + permission: Operations; additionalLock?: string[]; } @@ -73,7 +74,7 @@ export abstract class Query { public readonly needDocument: boolean; public readonly batchCompatible: boolean; public readonly additionalLock?: string[]; - public readonly permission: string; + public readonly permission: Operations; private readonly _runner: Runner; constructor( @@ -99,6 +100,7 @@ export abstract class Query { this.needDocument = data.needDocument; this.batchCompatible = data.batchCompatible; this.additionalLock = data.additionalLock; + this.permission = data.permission; this._runner = data.runner; } } @@ -151,14 +153,13 @@ export abstract class Query { ) { let perm = this.database.rules.hasPermission( this.query.path, + this.permission, this.session ); - if (this.permission === "read" && !perm.read) { - throw new QueryError("No permission!"); - } else if (this.permission === "write" && !perm.write) { - throw new QueryError("No permission!"); - } - this.query.path = perm.path; + + if (!perm) throw new QueryError("No permission!"); + + // this.query.path = perm.path; return this._runner.call( this, collection, @@ -173,14 +174,13 @@ export abstract class Query { ) { let perm = this.database.rules.hasPermission( this.query.path, + "read", this.session ); - if (!perm.read) { + if (!perm) { throw new QueryError("No permission!"); } - this.query.path = perm.path; - const receivedChanges = (changes: Change[]) => { let res = changes .filter((change) => this.checkChange(change)) @@ -464,7 +464,7 @@ export class CollectionQuery extends Query { batchCompatible: false, createCollection: false, needDocument: false, - permission: "read", + permission: "list", runner: this.keys, }; case "list": diff --git a/src/database/rules.ts b/src/database/rules.ts deleted file mode 100644 index 9f8c72e..0000000 --- a/src/database/rules.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Session from "./session"; -import Logging from "@hibas123/nodelogging"; - -interface IRule { - ".write"?: T; - ".read"?: T; -} - -type IRuleConfig = - | IRule - | { - [segment: string]: IRuleConfig; - }; - -type IRuleRaw = IRuleConfig; -type IRuleParsed = IRuleConfig; - -const resolve = (value: any) => { - if (value === true) { - return true; - } else if (typeof value === "string") { - } - return undefined; -}; - -export class Rules { - rules: IRuleParsed; - constructor(private config: string) { - let parsed: IRuleRaw = JSON.parse(config); - - const analyse = (raw: IRuleRaw) => { - let r: IRuleParsed = {}; - - if (raw[".read"]) { - let res = resolve(raw[".read"]); - if (res) { - r[".read"] = res; - } - delete raw[".read"]; - } - - if (raw[".write"]) { - let res = resolve(raw[".write"]); - if (res) { - r[".write"] = res; - } - delete raw[".write"]; - } - - for (let segment in raw) { - if (segment.startsWith(".")) continue; - - r[segment] = analyse(raw[segment]); - } - return r; - }; - - this.rules = analyse(parsed); - } - - hasPermission( - path: string[], - session: Session - ): { read: boolean; write: boolean; path: string[] } { - if (session.root) - return { - read: true, - write: true, - path: path - }; - let read = this.rules[".read"] || false; - let write = this.rules[".write"] || false; - - let rules = this.rules; - - for (let idx in path) { - let segment = path[idx]; - if (segment.startsWith(".")) { - read = false; - write = false; - Logging.log("Invalid query path (started with '$' or '.'):", path); - break; - } - - let k = Object.keys(rules) - .filter(e => e.startsWith("$")) - .find(e => { - switch (e) { - case "$uid": - if (segment === "$uid") { - path[idx] = session.uid; - return true; - } - if (segment === session.uid) return true; - break; - } - return false; - }); - - rules = (k ? rules[k] : undefined) || rules[segment] || rules["*"]; - - if (rules) { - if (rules[".read"]) { - read = rules[".read"]; - } - - if (rules[".write"]) { - read = rules[".write"]; - } - } else { - break; - } - } - - return { - read: read as boolean, - write: write as boolean, - path - }; - } - - toJSON() { - return this.config; - } -} diff --git a/src/rules/compile.ts b/src/rules/compile.ts new file mode 100644 index 0000000..0e0045e --- /dev/null +++ b/src/rules/compile.ts @@ -0,0 +1,296 @@ +import { + Node, + MatchStatement, + Operations, + ServiceStatement, + Expression, + ValueStatement, + Operators, +} from "./parser"; + +export class CompilerError extends Error { + node: Node; + constructor(message: string, node: Node) { + super(message); + this.node = node; + } +} + +type Variables = { [key: string]: string | Variables }; + +class Variable { + #name: string; + constructor(name: string) { + this.#name = name; + } + + getValue(variables: Variables) { + const parts = this.#name.split("."); + let current = variables as any; + while (parts.length > 0) { + const name = parts.shift(); + if (current && typeof current == "object") current = current[name]; + } + return current; + } +} + +class Value { + #value: any; + constructor(value: any) { + this.#value = value; + } + + get value() { + return this.#value; + } +} + +type ConditionParameters = Value | ConditionMatcher | Variable; +class ConditionMatcher { + #left: ConditionParameters; + #right: ConditionParameters; + #operator: Operators; + constructor( + left: ConditionParameters, + right: ConditionParameters, + operator: Operators + ) { + this.#left = left; + this.#right = right; + this.#operator = operator; + } + + test(variables: Variables): boolean { + let leftValue: any; + if (this.#left instanceof Value) { + leftValue = this.#left.value; + } else if (this.#left instanceof Variable) { + leftValue = this.#left.getValue(variables); + } else { + leftValue = this.#left.test(variables); + } + + let rightValue: any; + if (this.#right instanceof Value) { + rightValue = this.#right.value; + } else if (this.#right instanceof Variable) { + rightValue = this.#right.getValue(variables); + } else { + rightValue = this.#right.test(variables); + } + + switch (this.#operator) { + case "==": + return leftValue == rightValue; + case "!=": + return leftValue != rightValue; + case ">=": + return leftValue >= rightValue; + case "<=": + return leftValue <= rightValue; + case ">": + return leftValue > rightValue; + case "<": + return leftValue < rightValue; + case "&&": + return leftValue && rightValue; + case "||": + return leftValue || rightValue; + default: + throw new Error("Invalid operator " + this.#operator); + } + } +} + +class Rule { + #operation: Operations; + + #condition: ConditionParameters; + + get operation() { + return this.#operation; + } + + constructor(operation: Operations, condition: ConditionParameters) { + this.#operation = operation; + this.#condition = condition; + } + + test(variables: Variables): boolean { + if (this.#condition instanceof Value) { + return Boolean(this.#condition.value); + } else if (this.#condition instanceof Variable) { + return Boolean(this.#condition.getValue(variables)); + } else { + return this.#condition.test(variables); + } + } +} + +class Segment { + #name: string; + #variable: boolean; + get name() { + return this.#name; + } + constructor(name: string, variable = false) { + this.#name = name; + this.#variable = variable; + } + + match(segment: string): { match: boolean; variable?: string } { + return { + match: this.#name === segment || this.#variable, + variable: this.#variable && this.#name, + }; + } +} + +class Match { + #submatches: Match[]; + #rules: Rule[]; + + #segments: Segment[]; + #wildcard: boolean; + + constructor( + segments: Segment[], + rules: Rule[], + wildcard: boolean, + submatches: Match[] + ) { + this.#segments = segments; + this.#rules = rules; + this.#wildcard = wildcard; + this.#submatches = submatches; + } + + match( + segments: string[], + operation: Operations, + variables: Variables + ): boolean { + let localVars = { ...variables }; + if (segments.length >= this.#segments.length) { + for (let i = 0; i < this.#segments.length; i++) { + const match = this.#segments[i].match(segments[i]); + if (match.match) { + if (match.variable) { + localVars[match.variable] = segments[i]; + } + } else { + return false; + } + } + let remaining = segments.slice(this.#segments.length); + if (remaining.length > 0 && !this.#wildcard) { + for (const match of this.#submatches) { + const res = match.match(remaining, operation, localVars); + if (res) return true; + } + } else { + for (const rule of this.#rules) { + console.log(rule.operation, operation); + if (rule.operation === operation) { + if (rule.test(localVars)) return true; + } + } + } + } + + return false; + } +} + +export class RuleRunner { + #root_matches: Match[]; + constructor(root_matches: Match[]) { + this.#root_matches = root_matches; + } + + hasPermission(path: string[], operation: Operations, request: any): boolean { + if (request.root) return true; + for (const match of this.#root_matches) { + const res = match.match(path, operation, { request }); + if (res) return true; + } + return false; + } +} + +export function getRuleRunner(service: ServiceStatement) { + const createMatch = (s_match: MatchStatement) => { + let wildcard = false; + let segments = s_match.path.segments + .map((segment, idx, arr) => { + if (typeof segment === "string") { + if (segment === "*") { + if (idx === arr.length - 1) { + wildcard = true; + return null; + } else { + throw new CompilerError("Invalid path wildcard!", s_match); + } + } else { + return new Segment(segment, false); + } + } else { + return new Segment(segment.name, true); + } + }) + .filter((e) => e !== null); + + const resolveParameter = (e: Expression | ValueStatement) => { + let val: Value | ConditionMatcher | Variable; + if (e.type === "value") { + const c = e; + if (c.isFalse) { + val = new Value(false); + } else if (c.isTrue) { + val = new Value(true); + } else if (c.isNull) { + val = new Value(null); + } else if (c.isNumber) { + val = new Value(Number(c.value)); + } else if (c.isString) { + val = new Value(String(c.value)); + } else if (c.isVariable) { + val = new Variable(String(c.value)); + } else { + throw new CompilerError("Invalid value type!", e); + } + } else { + val = createCondition(e); + } + + return val; + }; + + const createCondition = (cond: Expression): ConditionMatcher => { + let left: ConditionParameters = resolveParameter(cond.left); + let right: ConditionParameters = resolveParameter(cond.right); + + return new ConditionMatcher(left, right, cond.operator); + }; + + const rules: Rule[] = s_match.rules + .map((rule) => { + const condition = resolveParameter(rule.condition); + return rule.operations.map((op) => new Rule(op, condition)); + }) + .flat(1); + const submatches = s_match.matches.map((sub) => createMatch(sub)); + const match = new Match(segments, rules, wildcard, submatches); + + console.log("Adding match", segments, rules, wildcard, submatches); + + return match; + }; + + const root_matches = service.matches.map((match) => createMatch(match)); + + const runner = new RuleRunner(root_matches); + + return runner; +} diff --git a/src/rules/error.ts b/src/rules/error.ts new file mode 100644 index 0000000..c2db018 --- /dev/null +++ b/src/rules/error.ts @@ -0,0 +1,36 @@ +export interface RuleError { + line: number; + column: number; + message: string; + original_err: Error; +} + +function indexToLineAndCol(src: string, index: number) { + let line = 1; + let col = 1; + for (let i = 0; i < index; i++) { + if (src.charAt(i) === "\n") { + line++; + col = 1; + } else { + col++; + } + } + + return { line, col }; +} + +export function transformError( + err: Error, + data: string, + idx: number +): RuleError { + let loc = indexToLineAndCol(data, idx); + + return { + line: loc.line, + column: loc.col, + message: err.message, + original_err: err, + }; +} diff --git a/src/rules/index.ts b/src/rules/index.ts new file mode 100644 index 0000000..f26ad54 --- /dev/null +++ b/src/rules/index.ts @@ -0,0 +1,30 @@ +import { RuleError, transformError } from "./error"; +import parse, { ParserError } from "./parser"; +import tokenize, { TokenizerError } from "./tokenise"; +import { getRuleRunner, RuleRunner } from "./compile"; +import { inspect } from "util"; + +export default function compileRule(rule: string) { + let runner: RuleRunner | undefined; + let error: RuleError | undefined; + try { + const tokenised = tokenize(rule); + // console.log(tokenised); + const parsed = parse(tokenised); + const dbservice = parsed.find((e) => e.name === "realtimedb"); + + if (!dbservice) throw new Error("No realtimedb service available!"); + + runner = getRuleRunner(dbservice); + } catch (err) { + if (err instanceof TokenizerError) { + error = transformError(err, rule, err.index); + } else if (err instanceof ParserError) { + error = transformError(err, rule, err.token.startIdx); + } else { + throw err; + } + } + + return { runner, error }; +} diff --git a/src/rules/parser.ts b/src/rules/parser.ts new file mode 100644 index 0000000..fb4411a --- /dev/null +++ b/src/rules/parser.ts @@ -0,0 +1,349 @@ +import { Token } from "./tokenise"; + +export interface Node { + type: string; + idx: number; +} + +export interface PathStatement extends Node { + type: "path"; + segments: (string | { type: "variable"; name: string })[]; +} + +export interface ValueStatement extends Node { + type: "value"; + isNull: boolean; + isTrue: boolean; + isFalse: boolean; + isNumber: boolean; + isString: boolean; + isVariable: boolean; + + value?: any; +} + +export type Operators = "&&" | "||" | "==" | "<=" | ">=" | "!=" | ">" | "<"; + +export interface Expression extends Node { + type: "expression"; + + left: ValueStatement | Expression; + operator: Operators; + right: ValueStatement | Expression; +} + +export type Operations = "read" | "write" | "list"; // | "update" | "create" | "delete" | "list"; + +export interface AllowStatement extends Node { + type: "permission"; + operations: Operations[]; + condition: Expression | ValueStatement; +} + +export interface MatchStatement extends Node { + type: "match"; + path: PathStatement; + matches: MatchStatement[]; + rules: AllowStatement[]; +} + +export interface ServiceStatement extends Node { + type: "service"; + name: string; + matches: MatchStatement[]; +} + +export class ParserError extends Error { + token: Token; + constructor(message: string, token: Token) { + super(message); + this.token = token; + } +} + +export default function parse(tokens: Token[]) { + const tokenIterator = tokens[Symbol.iterator](); + let currentToken: Token = tokenIterator.next().value; + let nextToken: Token = tokenIterator.next().value; + + const eatToken = (value?: string) => { + if (value && value !== currentToken.value) { + throw new ParserError( + `Unexpected token value, expected '${value}', received '${currentToken.value}'`, + currentToken + ); + } + let idx = currentToken.startIdx; + currentToken = nextToken; + nextToken = tokenIterator.next().value; + return idx; + }; + + const eatText = (): [string, number] => { + checkTypes("text"); + let val = currentToken.value; + let idx = currentToken.startIdx; + eatToken(); + return [val, idx]; + }; + const eatNumber = (): number => { + checkTypes("number"); + let val = Number(currentToken.value); + if (Number.isNaN(val)) { + throw new ParserError( + `Value cannot be parsed as number! ${currentToken.value}`, + currentToken + ); + } + eatToken(); + return val; + }; + + const checkTypes = (...types: string[]) => { + if (types.indexOf(currentToken.type) < 0) { + throw new ParserError( + `Unexpected token value, expected ${types.join(" | ")}, received '${ + currentToken.value + }'`, + currentToken + ); + } + }; + + const parsePathStatement = (): PathStatement => { + const segments: (string | { name: string; type: "variable" })[] = []; + const idx = currentToken.startIdx; + let next = currentToken.type === "slash"; + while (next) { + eatToken("/"); + if (currentToken.type === "curly_open" && nextToken.type === "text") { + eatToken("{"); + const [name] = eatText(); + segments.push({ + type: "variable", + name, + }); + eatToken("}"); + } else if (currentToken.type === "text") { + const [name] = eatText(); + segments.push(name); + } + next = currentToken.type === "slash"; + } + + return { + type: "path", + idx, + segments, + }; + }; + + const parseValue = (): ValueStatement => { + const idx = currentToken.startIdx; + + let isTrue = false; + let isFalse = false; + let isNull = false; + let isVariable = false; + let isNumber = false; + let isString = false; + let value: any = undefined; + if (currentToken.type === "keyword") { + if (currentToken.value === "true") isTrue = true; + else if (currentToken.value === "false") isFalse = true; + else if (currentToken.value === "null") isNull = true; + else { + throw new ParserError( + `Invalid keyword at this position ${currentToken.value}`, + currentToken + ); + } + eatToken(); + } else if (currentToken.type === "string") { + isString = true; + value = currentToken.value.slice(1, currentToken.value.length - 1); + eatToken(); + } else if (currentToken.type === "number") { + isNumber = true; + value = eatNumber(); + } else if (currentToken.type === "text") { + isVariable = true; + [value] = eatText(); + } else { + throw new ParserError( + `Expected value got ${currentToken.type}`, + currentToken + ); + } + + return { + type: "value", + isFalse, + isNull, + isNumber, + isString, + isTrue, + isVariable, + value, + idx, + }; + }; + + const parseCondition = (): Expression | ValueStatement => { + // let running = true; + let res: Expression | ValueStatement; + let left: Expression | ValueStatement | undefined; + + // while (running) { + const idx = currentToken.startIdx; + + if (!left) { + if (currentToken.type === "bracket_open") { + eatToken("("); + left = parseCondition(); + eatToken(")"); + } else { + left = parseValue(); + } + } + + if (currentToken.type === "comparison_operator") { + const operator = currentToken.value; + + eatToken(); + + let right: Expression | ValueStatement; + + let ct = currentToken; //Quick hack because of TypeScript + if (ct.type === "bracket_open") { + eatToken("("); + right = parseCondition(); + eatToken(")"); + } else { + right = parseValue(); + } + + res = { + type: "expression", + left, + right, + operator: operator as Operators, + idx, + }; + } else if (currentToken.type === "logic_operator") { + const operator = currentToken.value; + + eatToken(); + + const right = parseCondition(); + + res = { + type: "expression", + left, + operator: operator as Operators, + right, + idx, + }; + } else { + res = left; + } + + // let ct = currentToken; + // if ( + // ct.type === "comparison_operator" || + // ct.type === "logic_operator" + // ) { + // left = res; + // } else { + // running = false; + // } + // } + return res; + }; + + const parsePermissionStatement = (): AllowStatement => { + const idx = eatToken("allow"); + + const operations: Operations[] = []; + let next = currentToken.type !== "colon"; + while (next) { + const [operation] = eatText(); + operations.push(operation as Operations); + if (currentToken.type === "comma") { + next = true; + eatToken(","); + } else { + next = false; + } + } + + eatToken(":"); + + eatToken("if"); + + const condition = parseCondition(); + + eatToken(";"); + + return { + type: "permission", + idx, + operations, + condition, + }; + }; + + const parseMatchStatement = (): MatchStatement => { + const idx = eatToken("match"); + const path = parsePathStatement(); + + eatToken("{"); + const matches: MatchStatement[] = []; + const permissions: AllowStatement[] = []; + while (currentToken.type !== "curly_close") { + if (currentToken.value === "match") { + matches.push(parseMatchStatement()); + } else if (currentToken.value === "allow") { + permissions.push(parsePermissionStatement()); + } else { + throw new ParserError( + `Unexpected token value, expected 'match' or 'allow', received '${currentToken.value}'`, + currentToken + ); + } + } + eatToken("}"); + + return { + type: "match", + path, + idx, + matches, + rules: permissions, + }; + }; + + const parseServiceStatement = (): ServiceStatement => { + const idx = eatToken("service"); + let [name] = eatText(); + eatToken("{"); + const matches: MatchStatement[] = []; + while (currentToken.value === "match") { + matches.push(parseMatchStatement()); + } + eatToken("}"); + + return { + type: "service", + name: name, + idx, + matches, + }; + }; + + const nodes: ServiceStatement[] = []; + while (currentToken) { + nodes.push(parseServiceStatement()); + } + return nodes; +} diff --git a/src/rules/tokenise.ts b/src/rules/tokenise.ts new file mode 100644 index 0000000..04d2b82 --- /dev/null +++ b/src/rules/tokenise.ts @@ -0,0 +1,99 @@ +export type TokenTypes = + | "space" + | "comment" + | "string" + | "keyword" + | "colon" + | "semicolon" + | "comma" + | "comparison_operator" + | "logic_operator" + | "equals" + | "slash" + | "bracket_open" + | "bracket_close" + | "curly_open" + | "curly_close" + | "array" + | "questionmark" + | "number" + | "text"; + +export type Token = { + type: TokenTypes; + value: string; + startIdx: number; + endIdx: number; +}; + +type Matcher = (input: string, index: number) => undefined | Token; + +export class TokenizerError extends Error { + index: number; + constructor(message: string, index: number) { + super(message); + this.index = index; + } +} + +function regexMatcher(regex: string | RegExp, type: TokenTypes): Matcher { + if (typeof regex === "string") regex = new RegExp(regex); + + return (input: string, index: number) => { + let matches = input.substring(index).match(regex as RegExp); + if (!matches || matches.length <= 0) return undefined; + + return { + type, + value: matches[0], + startIdx: index, + endIdx: index + matches[0].length, + } as Token; + }; +} + +const matcher = [ + regexMatcher(/^\s+/, "space"), + regexMatcher(/^(\/\*)(.|\s)*?(\*\/)/g, "comment"), + regexMatcher(/^\/\/.+/, "comment"), + regexMatcher(/^#.+/, "comment"), + regexMatcher(/^".*?"/, "string"), + // regexMatcher(/(?<=^")(.*?)(?=")/, "string"), + regexMatcher(/^(service|match|allow|if|true|false|null)/, "keyword"), + regexMatcher(/^\:/, "colon"), + regexMatcher(/^\;/, "semicolon"), + regexMatcher(/^\,/, "comma"), + regexMatcher(/^(\=\=|\!\=|\<\=|\>\=|\>|\<)/, "comparison_operator"), + regexMatcher(/^(&&|\|\|)/, "logic_operator"), + regexMatcher(/^\=/, "equals"), + regexMatcher(/^\//, "slash"), + regexMatcher(/^\(/, "bracket_open"), + regexMatcher(/^\)/, "bracket_close"), + regexMatcher(/^{/, "curly_open"), + regexMatcher(/^}/, "curly_close"), + regexMatcher(/^\[\]/, "array"), + regexMatcher(/^\?/, "questionmark"), + regexMatcher(/^[0-9]+(\.[0-9]+)?/, "number"), + regexMatcher(/^[a-zA-Z_\*]([a-zA-Z0-9_\.\*]?)+/, "text"), +]; + +export default function tokenize(input: string) { + let index = 0; + let tokens: Token[] = []; + while (index < input.length) { + const matches = matcher.map((m) => m(input, index)).filter((e) => !!e); + let match = matches[0]; + if (match) { + if (match.type !== "space" && match.type !== "comment") { + tokens.push(match); + } + index += match.value.length; + } else { + throw new TokenizerError( + `Unexpected token '${input.substring(index, index + 1)}'`, + index + ); + } + } + return tokens; +} diff --git a/src/web/helper/form.ts b/src/web/helper/form.ts index 007542d..901d267 100644 --- a/src/web/helper/form.ts +++ b/src/web/helper/form.ts @@ -2,7 +2,7 @@ import { getTemplate } from "./hb"; import { Context } from "vm"; interface IFormConfigField { - type: "text" | "number" | "boolean" | "textarea"; + type: "text" | "number" | "boolean" | "textarea" | "codemirror"; label: string; value?: string; disabled?: boolean; @@ -15,16 +15,16 @@ export default function getForm( title: string, fieldConfig: IFormConfig ): (ctx: Context) => void { - let fields = Object.keys(fieldConfig).map(name => ({ + let fields = Object.keys(fieldConfig).map((name) => ({ name, ...fieldConfig[name], - disabled: fieldConfig.disabled ? "disabled" : "" + disabled: fieldConfig.disabled ? "disabled" : "", })); - return ctx => + return (ctx) => (ctx.body = getTemplate("forms")({ url, title, - fields + fields, })); } diff --git a/src/web/v1/admin.ts b/src/web/v1/admin.ts index 1ea7a83..d9b8f28 100644 --- a/src/web/v1/admin.ts +++ b/src/web/v1/admin.ts @@ -5,7 +5,7 @@ import getTable from "../helper/table"; import { BadRequestError, NoPermissionError, - NotFoundError + NotFoundError, } from "../helper/errors"; import { DatabaseManager } from "../../database/database"; import { MP } from "../../database/query"; @@ -21,17 +21,17 @@ AdminRoute.use(async (ctx, next) => { return next(); }); -AdminRoute.get("/", async ctx => { +AdminRoute.get("/", async (ctx) => { //TODO: Main Interface ctx.body = getView("admin"); }); -AdminRoute.get("/settings", async ctx => { +AdminRoute.get("/settings", async (ctx) => { let res = await new Promise((yes, no) => { const stream = Settings.db.createReadStream({ keys: true, values: true, - valueAsBuffer: true + valueAsBuffer: true, }); let res = [["key", "value"]]; stream.on("data", ({ key, value }) => { @@ -49,7 +49,7 @@ AdminRoute.get("/settings", async ctx => { } }); -AdminRoute.get("/data", async ctx => { +AdminRoute.get("/data", async (ctx) => { const { database } = ctx.query; let db = DatabaseManager.getDatabase(database); if (!db) throw new BadRequestError("Database not found"); @@ -59,7 +59,7 @@ AdminRoute.get("/data", async ctx => { values: true, valueAsBuffer: true, keyAsBuffer: false, - limit: 1000 + limit: 1000, }); let res = [["key", "value"]]; stream.on("data", ({ key, value }: { key: string; value: Buffer }) => { @@ -67,7 +67,7 @@ AdminRoute.get("/data", async ctx => { key, key.split("/").length > 2 ? value.toString() - : JSON.stringify(MP.decode(value)) + : JSON.stringify(MP.decode(value)), ]); }); @@ -82,7 +82,7 @@ AdminRoute.get("/data", async ctx => { } }); -AdminRoute.get("/database", ctx => { +AdminRoute.get("/database", (ctx) => { const isFull = ctx.query.full === "true" || ctx.query.full === "1"; let res; if (isFull) { @@ -90,7 +90,7 @@ AdminRoute.get("/database", ctx => { res = Array.from(DatabaseManager.databases.entries()).map( ([name, config]) => ({ name, - ...JSON.parse(JSON.stringify(config)) + ...JSON.parse(JSON.stringify(config)), }) ); } else { @@ -102,7 +102,7 @@ AdminRoute.get("/database", ctx => { } else { ctx.body = res; } -}).post("/database", async ctx => { +}).post("/database", async (ctx) => { const { name, rules, publickey, accesskey, rootkey } = ctx.request.body; if (!name) throw new BadRequestError("Name must be set!"); @@ -121,7 +121,7 @@ AdminRoute.get("/database", ctx => { ctx.body = "Success"; }); -AdminRoute.get("/collections", async ctx => { +AdminRoute.get("/collections", async (ctx) => { const { database } = ctx.query; let db = DatabaseManager.getDatabase(database); if (!db) throw new BadRequestError("Database not found"); @@ -129,7 +129,7 @@ AdminRoute.get("/collections", async ctx => { let res = await new Promise((yes, no) => { const stream = db.collections.createKeyStream({ keyAsBuffer: false, - limit: 1000 + limit: 1000, }); let res = []; stream.on("data", (key: string) => { @@ -147,7 +147,7 @@ AdminRoute.get("/collections", async ctx => { } }); -AdminRoute.get("/collections/cleanup", async ctx => { +AdminRoute.get("/collections/cleanup", async (ctx) => { const { database } = ctx.query; let db = DatabaseManager.getDatabase(database); if (!db) throw new BadRequestError("Database not found"); @@ -169,13 +169,13 @@ AdminRoute.get( rules: { label: "Rules", type: "textarea", - value: `{\n ".write": true, \n ".read": true \n}` + value: `{\n ".write": true, \n ".read": true \n}`, }, - publickey: { label: "Public Key", type: "textarea" } + publickey: { label: "Public Key", type: "textarea" }, }) ); -AdminRoute.get("/database/update", async ctx => { +AdminRoute.get("/database/update", async (ctx) => { const { database } = ctx.query; let db = DatabaseManager.getDatabase(database); if (!db) throw new NotFoundError("Database not found!"); @@ -184,28 +184,28 @@ AdminRoute.get("/database/update", async ctx => { label: "Name", type: "text", value: db.name, - disabled: true + disabled: true, }, accesskey: { label: "Access Key", type: "text", - value: db.accesskey + value: db.accesskey, }, rootkey: { label: "Root access key", type: "text", - value: db.rootkey + value: db.rootkey, }, rules: { label: "Rules", - type: "textarea", - value: db.rules.toJSON() + type: "codemirror", + value: db.rawRules, }, publickey: { label: "Public Key", type: "textarea", - value: db.publickey - } + value: db.publickey, + }, })(ctx); }); diff --git a/tsconfig.json b/tsconfig.json index 06472e3..2dbe6bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,17 @@ { - "compilerOptions": { - /* Basic Options */ - "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "declaration": true, /* Generates corresponding '.d.ts' file. */ - "sourceMap": true, /* Generates corresponding '.map' file. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "strict": false, /* Enable all strict type-checking options. */ - "preserveWatchOutput": true, - "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "resolveJsonModule": true - }, - "exclude": [ - "node_modules/" - ], - "include": [ - "./src" - ] -} \ No newline at end of file + "compilerOptions": { + /* Basic Options */ + "target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "declaration": true /* Generates corresponding '.d.ts' file. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + "outDir": "./lib" /* Redirect output structure to the directory. */, + "strict": false /* Enable all strict type-checking options. */, + "preserveWatchOutput": true, + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, + "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, + "resolveJsonModule": true + }, + "exclude": ["node_modules/"], + "include": ["./src"] +} diff --git a/views/admin.html b/views/admin.html index a51fb85..497d4e9 100644 --- a/views/admin.html +++ b/views/admin.html @@ -33,12 +33,19 @@ grid-template-columns: 360px auto; } + #navigation { + height: 100vh; + overflow: auto; + border-right: 1px solid darkgrey; + padding: 1rem; + } + #content { position: absolute; top: 0; left: 0; width: 100%; - height: 100%; + height: 100vh; border: 0; } @@ -46,7 +53,7 @@
-
+ -
+
@@ -101,8 +108,8 @@

${database}

- +
` ) ) diff --git a/views/forms.hbs b/views/forms.hbs index dcf0000..bd2dd2f 100644 --- a/views/forms.hbs +++ b/views/forms.hbs @@ -8,6 +8,7 @@ {{title}} +