├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── bkjs ├── examples ├── alpine │ ├── README.md │ ├── app.sh │ ├── etc │ │ └── config │ ├── modules │ │ └── bk_data.js │ ├── package.json │ └── web │ │ ├── components.js │ │ └── index.html ├── blog │ ├── README.md │ ├── app.js │ ├── app.sh │ ├── etc │ │ └── config │ ├── package.json │ └── web │ │ ├── index.css │ │ ├── index.html │ │ └── index.js ├── modules │ ├── REAME.md │ ├── amqp.js │ ├── apn.js │ ├── bk_data.js │ ├── bk_dynamodbstreams.js │ ├── bk_file.js │ ├── bk_system.js │ ├── dbp_cassandra.js │ ├── dbp_couchdb.js │ ├── dbp_mongodb.js │ ├── dbp_mysql.js │ ├── dbp_redis.js │ ├── dbp_riak.js │ ├── dynamodbstreams.js │ ├── fcm.js │ ├── ipcc_db.js │ ├── ipcc_hazelcast.js │ ├── ipcc_memcache.js │ └── msg_sns.js └── srp │ ├── README.md │ ├── app.js │ ├── app.sh │ ├── package.json │ ├── test.js │ └── web │ ├── bkjs-srp.js │ ├── index.html │ └── jsbn.js ├── lib ├── api.js ├── api │ ├── acl.js │ ├── auth.js │ ├── csrf.js │ ├── files.js │ ├── hooks.js │ ├── icons.js │ ├── passkeys.js │ ├── session.js │ ├── sig.js │ ├── users.js │ ├── utils.js │ └── ws.js ├── app.js ├── aws.js ├── aws │ ├── cw.js │ ├── dynamodb.js │ ├── ec2.js │ ├── ecs.js │ ├── meta.js │ ├── other.js │ ├── query.js │ ├── route53.js │ ├── s3.js │ ├── ses.js │ ├── sns.js │ └── sqs.js ├── cache.js ├── cache │ ├── client.js │ ├── local.js │ ├── redis.js │ └── worker.js ├── core.js ├── core │ ├── args.js │ ├── sendgrid.js │ └── utils.js ├── db.js ├── db │ ├── cache.js │ ├── config.js │ ├── dynamodb.js │ ├── elasticsearch.js │ ├── pg.js │ ├── pools.js │ ├── prepare.js │ ├── sql.js │ ├── sqlite.js │ └── utils.js ├── events.js ├── httpget.js ├── index.js ├── ipc.js ├── jobs.js ├── lib.js ├── lib │ ├── conv.js │ ├── crypto.js │ ├── file.js │ ├── flow.js │ ├── hash.js │ ├── is.js │ ├── lru.js │ ├── obj.js │ ├── parse.js │ ├── str.js │ ├── system.js │ ├── time.js │ └── uuid.js ├── logger.js ├── logger │ └── syslog.js ├── logwatcher.js ├── metrics.js ├── metrics │ ├── Counter.js │ ├── ExponentiallyMovingWeightedAverage.js │ ├── FakeTrace.js │ ├── Histogram.js │ ├── Meter.js │ ├── Timer.js │ ├── TokenBucket.js │ └── Trace.js ├── pool.js ├── push.js ├── push │ └── webpush.js ├── queue.js ├── queue │ ├── client.js │ ├── local.js │ ├── nats.js │ ├── redis.js │ ├── sqs.js │ └── worker.js ├── run.js ├── server.js ├── shell.js ├── shell │ ├── aws.js │ ├── db.js │ ├── shell.js │ ├── test.js │ └── user.js ├── stats.js ├── users.js └── watch.js ├── package.json ├── tests ├── api.js ├── auth.js ├── cache.js ├── config ├── core.js ├── db.js ├── ipc.js ├── jobs.js └── lib.js ├── tools ├── alpine │ └── APKBUILD.cloudwatch ├── bkjs-alpine ├── bkjs-bundle ├── bkjs-deps ├── bkjs-docker ├── bkjs-dynamodb ├── bkjs-ec2 ├── bkjs-ec2-ami ├── bkjs-ec2-cwagent ├── bkjs-ecr ├── bkjs-ecs ├── bkjs-ecs-agent ├── bkjs-elasticsearch ├── bkjs-es ├── bkjs-get ├── bkjs-install ├── bkjs-monit ├── bkjs-nats ├── bkjs-redis ├── bkjs-setup ├── bkjs-sync ├── bkjs-test ├── doc.js ├── docker │ └── Dockerfile.abuild ├── endpoints.js └── locales.js └── web ├── css ├── bkjs.bundle.css ├── bootstrap.css ├── doc.css ├── font-awesome.css └── index.html ├── doc.html ├── img ├── 1.png ├── index.html ├── loading.gif └── logo.png ├── index.html ├── js ├── alpine.csp.js ├── alpine.js ├── alpine.mask.js ├── app.js ├── app.ko.js ├── bkjs-bootstrap.js ├── bkjs-conv.js ├── bkjs-crypto.js ├── bkjs-ko.js ├── bkjs-lib.js ├── bkjs-passkey.js ├── bkjs-send.js ├── bkjs-user.js ├── bkjs-ws.js ├── bkjs.bundle.js ├── bootpopup.js ├── bootstrap.js ├── bootstrap.min.js ├── index.html ├── jquery3.js ├── jquery3.min.js ├── jquery3.slim.js ├── jquery3.slim.min.js ├── knockout.js ├── knockout.min.js ├── popper2.js ├── popper2.min.js ├── webauthn.min.mjs └── webpush.js └── webfonts ├── fa-brands-400.eot ├── fa-brands-400.svg ├── fa-brands-400.ttf ├── fa-brands-400.woff ├── fa-brands-400.woff2 ├── fa-regular-400.eot ├── fa-regular-400.svg ├── fa-regular-400.ttf ├── fa-regular-400.woff ├── fa-regular-400.woff2 ├── fa-solid-900.eot ├── fa-solid-900.svg ├── fa-solid-900.ttf ├── fa-solid-900.woff ├── fa-solid-900.woff2 └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .DS_Store 3 | *.[aod] 4 | *.gch 5 | *.so 6 | *.sqlext 7 | *.dSYM 8 | *.node 9 | *.swp 10 | *.*~ 11 | *.log 12 | *.gz 13 | *.br 14 | *.tgz 15 | *.zip 16 | *.js.map 17 | node_modules/ 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2013 Vlad Seryakov. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/alpine/README.md: -------------------------------------------------------------------------------- 1 | # Backend.js sample config application with Alpine.js 2 | 3 | 1. Create tables 4 | 5 | ./app.sh -db-create-tables -shell 6 | 7 | 3. Run the app 8 | 9 | ./app.sh 10 | 11 | 4. Point browser to http://localhost:8000 12 | 13 | 14 | # Authors 15 | vlad 16 | 17 | -------------------------------------------------------------------------------- /examples/alpine/app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec ../../bkjs run -api -watch $(pwd) -etc-dir $(pwd)/etc -web-path ~~$(pwd)/web -modules-path ~~$(pwd)/modules "$@" 4 | 5 | -------------------------------------------------------------------------------- /examples/alpine/etc/config: -------------------------------------------------------------------------------- 1 | 2 | repl-port-api=2090 3 | 4 | api-allow-path=/ 5 | api-routing-^/app=/index.html 6 | 7 | db-pool=dynamodb 8 | db-dynamodb-pool=http://localhost:8181 9 | 10 | -------------------------------------------------------------------------------- /examples/alpine/modules/bk_data.js: -------------------------------------------------------------------------------- 1 | ../../modules/bk_data.js -------------------------------------------------------------------------------- /examples/alpine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "author": "vlad", 4 | "name": "app", 5 | "description": "The backend", 6 | "repository": { "type": "git", "url": "http://github.com/vseryakov/backendjs.git" }, 7 | "main": "app.js", 8 | "dependencies": { 9 | "backendjs": ">=0.9.0" 10 | }, 11 | "engines": { "node": ">= 0.10" }, 12 | "scripts": { "start": "app.sh" }, 13 | "license": "BSD-3-Clause" } 14 | -------------------------------------------------------------------------------- /examples/blog/README.md: -------------------------------------------------------------------------------- 1 | # Backend.js blog 2 | 3 | 1. To install 4 | 5 | run npm install 6 | 7 | 2. Create a user 8 | 9 | bksh -home . -user-add login test secret test 10 | 11 | 3. Run the app 12 | 13 | ./app.sh 14 | 15 | 4. Point browser to http://localhost:8000 16 | 17 | 5. Login as test:test 18 | 19 | # Authors 20 | vlad 21 | 22 | -------------------------------------------------------------------------------- /examples/blog/app.js: -------------------------------------------------------------------------------- 1 | // 2 | // Blog app 3 | // Created by vlad on Thu Sep 28 12:48:54 EDT 2014 4 | // 5 | var { db, api, app, server } = require('backendjs'); 6 | 7 | // Add custom properties to the existing table 8 | db.describeTables({ 9 | bk_message: { 10 | title: {}, 11 | tags: {}, 12 | }, 13 | }); 14 | 15 | // This is called after the database pools are initialized, produce 16 | // icon properties for each record on every read 17 | app.configureModule = function(options, callback) 18 | { 19 | db.setProcessRow("post", "bk_message", function(req, row, options) { 20 | if (!row.sender) return; 21 | row.avatar = '/image/user/' + row.sender + "/0"; 22 | if (row.icon) row.icon = '/image/blog/' + row.id + '/' + row.mtime + ':' + row.sender; 23 | }); 24 | callback(); 25 | } 26 | 27 | app.configureWeb = function(options, callback) 28 | { 29 | this.initBlogAPI(); 30 | callback(); 31 | } 32 | 33 | app.initBlogAPI = function() 34 | { 35 | // Return images by prefix, id and possibly type 36 | api.app.all(/^\/image\/([a-zA-Z0-9_.:-]+)\/([^/ ]+)\/?([^/ ]+)?$/, (req, res) => { 37 | var options = api.getOptions(req); 38 | options.prefix = req.params[0]; 39 | options.type = req.params[2]; 40 | var id = req.params[1]; 41 | // Image extension at the end so it looks like an image path 42 | if (options.type) { 43 | const d = options.type.match(/^(.+)\.(png|jpg|jpeg|gif)$/); 44 | if (d) options.type = d[1], options.ext = d[2]; 45 | } else { 46 | const d = id.match(/^(.+)\.(png|jpg|jpeg|gif)$/); 47 | if (d) id = d[1], options.ext = d[2]; 48 | } 49 | api.sendIcon(req, id, options); 50 | }); 51 | 52 | api.app.all(/^\/blog\/([a-z\/]+)$/, function(req, res) { 53 | var options = api.getOptions(req); 54 | 55 | // This is our global blog id 56 | req.query.id = "0"; 57 | 58 | switch (req.params[0]) { 59 | case "select": 60 | options.ops = { mtime: "gt" }; 61 | options.count = 5; 62 | options.desc = 1; 63 | 64 | db.select("bk_message", req.query, options, function(err, rows, info) { 65 | if (err) return api.sendReply(res, err); 66 | res.json(api.getResultPage(req, options, rows, info)); 67 | }); 68 | break; 69 | 70 | case "get": 71 | if (!req.query.mtime) return api.sendReply(res, 400, "no mtime provided"); 72 | req.query.sender = req.user.id; 73 | req.query.mtime += ":" + req.query.sender; 74 | 75 | db.get("bk_message", { id: req.query.id, mtime: req.query.mtime }, options, function(err, row) { 76 | if (err) return api.sendReply(res, err); 77 | res.json(row); 78 | }); 79 | break; 80 | 81 | case "put": 82 | if (!req.query.sender) req.query.sender = req.user.id; 83 | if (!req.query.mtime) req.query.mtime = Date.now(); 84 | req.query.mtime += ":" + req.query.sender; 85 | req.query.name = req.user.name; 86 | 87 | api.putIcon(req, "icon", req.query.id, { prefix: 'blog', type: req.query.mtime }, function(err, icon) { 88 | if (err) return api.sendReply(res, err); 89 | 90 | req.query.icon = icon ? 1 : 0; 91 | db.put("bk_message", req.query, function(err) { 92 | api.sendReply(res, err); 93 | }); 94 | }); 95 | break; 96 | 97 | case "del": 98 | if (!req.query.mtime) return api.sendReply(res, 400, "no mtime provided"); 99 | if (!req.query.sender) return api.sendReply(res, 400, "no sender provided"); 100 | req.query.mtime += ":" + req.query.sender; 101 | 102 | db.del("bk_message", { id: req.query.id, mtime: req.query.mtime }, options, function(err) { 103 | if (err) return api.sendReply(res, err); 104 | 105 | api.delIcon(req.query.id, { prefix: "blog", type: req.query.mtime }, function() { 106 | api.sendReply(res, err); 107 | }); 108 | }); 109 | break; 110 | 111 | default: 112 | api.sendReply(res, 400, "invalid command"); 113 | } 114 | }); 115 | } 116 | 117 | // It is called before processing all requests, just after the user was verified 118 | api.registerPreProcess('', /^\//, function(req, status, callback) 119 | { 120 | if (status && status.status != 200) { 121 | // Allow access to blog list without an user 122 | if (status.status == 404 && req.path.match(/^\/blog\/select/)) status = null; 123 | return callback(status); 124 | } 125 | 126 | callback(); 127 | }); 128 | 129 | server.start(); 130 | -------------------------------------------------------------------------------- /examples/blog/app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec node app.js -api -watch `pwd` -web -log debug -etc-dir `pwd`/etc -web-path `pwd`/web $@ 4 | 5 | -------------------------------------------------------------------------------- /examples/blog/etc/config: -------------------------------------------------------------------------------- 1 | api-allow-path=/ 2 | 3 | -------------------------------------------------------------------------------- /examples/blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "author": "vlad", 4 | "name": "blog", 5 | "description": "simple blog example", 6 | "main": "app.js", 7 | "dependencies": { "backendjs": ">=0.9.0" }, 8 | "engines": { "node": ">= 0.10" }, 9 | "scripts": { "start": "node app.js" }, 10 | "license": "BSD-3-Clause" } 11 | -------------------------------------------------------------------------------- /examples/blog/web/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | background-color: #ffffff; 4 | } 5 | 6 | #logo { 7 | height: 22px; 8 | } 9 | 10 | #blog #header { 11 | border-bottom: 1px solid gray; 12 | } 13 | 14 | #blog .container { 15 | padding-bottom: 20px; 16 | } 17 | 18 | #blog .tags { 19 | background-color: #2F9888; 20 | color: white; 21 | padding: 3px; 22 | } 23 | 24 | #blog-content b { 25 | font-size: 1.1em; 26 | } 27 | 28 | #blog-content .row { 29 | padding-bottom: 10px; 30 | } 31 | 32 | .form-error { 33 | color: red; 34 | } 35 | 36 | .img-avatar { 37 | width: 32px; 38 | height: 32px; 39 | border: 0; 40 | } 41 | 42 | .img-post { 43 | max-height: 100px; 44 | border: 0; 45 | } 46 | 47 | .blog-preview { 48 | padding: 5px; 49 | width: 50px; 50 | height: 50px; 51 | } 52 | 53 | .blog-content { 54 | overflow-y: auto; 55 | max-height: 1200px; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /examples/blog/web/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Vlad Seryakov 20014 3 | // 4 | 5 | var self = bkjs; 6 | self.session = true; 7 | self.auth = ko.observable(0); 8 | self.blog = ko.observableArray([]); 9 | self.blog_token = null; 10 | 11 | self.showAlert = function(type, text) 12 | { 13 | $("#form-error").append("" + text + ""); 14 | $("#form-error span").last().hide().fadeIn(200).delay(5000 + (type == "error" ? 5000 : 0)).fadeOut(1000, function () { $(this).remove(); }); 15 | } 16 | 17 | self.previewImage = function(input) 18 | { 19 | if (!input || !input.files || !input.files[0]) { 20 | $("#" + input.id + '-preview').attr('src', '#'); 21 | return; 22 | } 23 | var reader = new FileReader(); 24 | reader.onload = function(e) { $("#" + input.id + '-preview').attr('src', e.target.result); } 25 | reader.readAsDataURL(input.files[0]); 26 | } 27 | 28 | self.onImageError = function(data, event) 29 | { 30 | $(event.currentTarget).context.src = "/img/1.png"; 31 | } 32 | 33 | self.showBlog = function(data, event) 34 | { 35 | if (self.blog_token === "") return; 36 | self.send({ url: '/blog/select', data: { _start: self.blog_token } }, function(data) { 37 | self.blog_token = data.next_token || ""; 38 | data.data.forEach(function(x) { 39 | x.tags = x.tags ? x.tags.split(" ") : []; 40 | x.ctime = x.mtime; 41 | x.icon = x.icon || '/img/1.png'; 42 | x.mtime = x.mtime ? self.strftime(x.mtime, "%a, %b %d %Y, %H:%M%p") : ""; 43 | }); 44 | self.blog(data.data); 45 | }); 46 | } 47 | 48 | self.postBlog = function(data, event) 49 | { 50 | var obj = {}; 51 | ["msg","title","tags","mtime","sender"].forEach(function(x) { obj[x] = $("#blog-" + x).val(); }); 52 | if (!obj.msg) return; 53 | 54 | var img = $("#blog-img"); 55 | if (img[0].files && img[0].files.length) { 56 | self.sendFile({ file: img[0], url: '/blog/put', data: obj, callback: function() { 57 | self.blog_token = null; 58 | self.showBlog(); 59 | $('#blog-form').modal("hide"); 60 | } }); 61 | return; 62 | } 63 | self.send({ url: '/blog/put', data: obj, type: "POST" }, function() { 64 | self.blog_token = null; 65 | self.showBlog(); 66 | $('#blog-form').modal("hide"); 67 | }, function(err) { 68 | self.showAlert("error", err); 69 | $('#blog-form').modal("hide"); 70 | }); 71 | } 72 | 73 | self.editBlog = function(data, event) 74 | { 75 | if (!data || !data.ctime) { 76 | ["msg","title","tags","mtime","sender"].forEach(function(x) { $("#blog-" + x).val(""); }); 77 | $('#blog-form').modal("show"); 78 | return; 79 | } 80 | self.send({ url: '/blog/get', data: { mtime: data.ctime, sender: data.sender }, type: "POST" }, function(obj) { 81 | ["msg","title","tags","mtime","sender"].forEach(function(x) { $("#blog-" + x).val(obj[x]); }); 82 | $('#blog-form').modal("show"); 83 | }, function(err) { 84 | self.showAlert("error", err); 85 | }); 86 | } 87 | 88 | self.delBlog = function(data, event) 89 | { 90 | if (!confirm("Delete this post?")) return; 91 | self.send({ url: '/blog/del', data: { mtime: data.ctime, sender: data.sender }, type: "POST" }, function() { 92 | self.blog_token = null; 93 | self.showBlog(); 94 | }, function(err) { 95 | self.showAlert("error", err); 96 | }); 97 | } 98 | 99 | self.doLogin = function(data, event) 100 | { 101 | self.login($('#login').val(), $('#secret').val(), function(err) { 102 | if (err) { 103 | $('#secret').val(''); 104 | $('#login-form').modal("show"); 105 | return; 106 | } 107 | $('#login-form').modal("hide"); 108 | self.auth(self.loggedIn); 109 | self.showBlog(); 110 | }); 111 | } 112 | 113 | $(function() { 114 | 115 | $("input[type=file]").change(function() { self.previewImage(this); }); 116 | 117 | // Autofocus for dialogs 118 | $(".modal").on('shown.bs.modal', function () { 119 | lastfocus = $(this); 120 | $(this).find('input:text:visible:first').focus(); 121 | }); 122 | 123 | // Auto load more items on scroll 124 | $('#blog-content').scroll(function() { 125 | if ($('#blog-content').scrollTop() >= $('#blog-content').prop('scrollHeight') - ($('#blog-content').prop('clientHeight'))) { 126 | self.showBlog(); 127 | } 128 | }); 129 | 130 | ko.applyBindings(self); 131 | self.login(function() { 132 | self.auth(self.loggedIn); 133 | self.showBlog(); 134 | }); 135 | 136 | }); 137 | 138 | -------------------------------------------------------------------------------- /examples/modules/REAME.md: -------------------------------------------------------------------------------- 1 | # Experimental, legacy or obsolete modules 2 | 3 | Most will not work but kept for references. 4 | 5 | -------------------------------------------------------------------------------- /examples/modules/amqp.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var util = require('util'); 7 | var logger = require(__dirname + '/../logger'); 8 | var lib = require(__dirname + '/../lib'); 9 | var Client = require(__dirname + "/client"); 10 | 11 | // Queue client using RabbitMQ server 12 | // 13 | // To enable install the npm module: 14 | // 15 | // npm i -g amqplib 16 | // 17 | const client = { 18 | name: "amqp", 19 | 20 | createClient: function(options) { 21 | if (/^amqps?:/.test(options?.url)) return new AmqpClient(options); 22 | } 23 | }; 24 | module.exports = client; 25 | 26 | function AmqpClient(options) 27 | { 28 | Client.call(this, options); 29 | if (!lib.isObject(this.options.sockParams)) this.options.sockParams = {}; 30 | if (!lib.isObject(this.options.channelParams)) this.options.channelParams = {}; 31 | if (!lib.isObject(this.options.consumeParams)) this.options.consumeParams = {}; 32 | 33 | var amqp = require("amqplib/callback_api"); 34 | amqp.connect(this.url, this.options.sockParams, (err, conn) => { 35 | if (err) return logger.error("amqp:", this.url, err); 36 | this.client = conn; 37 | conn.on("error", (err) => { logger.error("amqp:", this.url, err) }); 38 | conn.createChannel((err, ch) => { 39 | if (err) return logger.error("amqp:", "create", this.url, err); 40 | ch.on("error", (err) => { logger.error("amqp:", this.url, err) }); 41 | this.channel = ch; 42 | this.emit("ready"); 43 | }); 44 | }); 45 | } 46 | util.inherits(AmqpClient, Client); 47 | 48 | AmqpClient.prototype.close = function() 49 | { 50 | Client.prototype.close.call(this); 51 | if (this.client) this.client.close(); 52 | delete this.channel; 53 | } 54 | 55 | AmqpClient.prototype.pollQueue = function(options) 56 | { 57 | if (!this.channel) return; 58 | if (this.options.count) { 59 | this.channel.prefetch(this.options.count); 60 | } 61 | var done, chan = this.channel(options); 62 | this.channel.consume(chan, (item) => { 63 | if (item === null) return; 64 | var msg = lib.jsonParse(item.content.toString(), { url: this.url, datatype: "obj", logger: "error" }); 65 | logger.debug("amqp:", chan, "MSG:", msg, "ITEM:", item); 66 | 67 | if (!this.emit(chan, msg, (err) => { 68 | if (done || this.options.consumeParams.noAck) return; 69 | done = 1; 70 | if (err && err.status >= 500) return this.channel.nack(item); 71 | this.channel.ack(item); 72 | })) { 73 | done = 1; 74 | if (!this.options.consumeParams.noAck) this.channel.nack(item); 75 | } 76 | }, this.options.consumeParams, (err, ok) => { 77 | logger.logger(err ? "error": "debug", "amqp:", "consume", chan, err, ok); 78 | if (!err) this.tag = ok.consumerTag; 79 | }); 80 | } 81 | 82 | AmqpClient.prototype.subscribeQueue = function(options, callback) 83 | { 84 | if (!this.channel) return; 85 | var chan = this.channel(options); 86 | this.channel.assertQueue(chan, this.options.channelParams, (err) => { 87 | if (err) return logger.error("amqp:", "assert", this.url, err); 88 | Client.prototype.subscribeQueue.call(this, options, callback); 89 | }); 90 | } 91 | 92 | AmqpClient.prototype.unsubscribeQueue = function(options, callback) 93 | { 94 | Client.prototype.unsubscribeQueue.call(this, options, callback); 95 | if (this.channel && this.tag) this.channel.cancel(this.tag); 96 | } 97 | 98 | AmqpClient.prototype.publishQueue = function(msg, options, callback) 99 | { 100 | if (this.channel) { 101 | var chan = this.channel(options); 102 | this.channel.sendToQueue(chan, Buffer.from(msg)); 103 | } 104 | lib.tryCall(callback, this.channel ? null : { status: 400, message: "not open" }); 105 | } 106 | -------------------------------------------------------------------------------- /examples/modules/bk_data.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const bkjs = require('backendjs'); 7 | const db = bkjs.db; 8 | const api = bkjs.api; 9 | const lib = bkjs.lib; 10 | 11 | // Data management 12 | const mod = { 13 | name: "bk_data", 14 | args: [ 15 | { name: "perms", type: "map", maptype: "list", descr: "Tables and allowed operations, ex: -bk_data-perms bk_config:select;put" }, 16 | ], 17 | controls: { 18 | region: { type: "string" }, 19 | pool: { type: "string" }, 20 | }, 21 | }; 22 | module.exports = mod; 23 | 24 | // Create API endpoints and routes 25 | mod.configureWeb = function(options, callback) 26 | { 27 | api.registerControlParams(mod.controls); 28 | this.configureDataAPI(); 29 | callback() 30 | } 31 | 32 | // API for full access to all tables 33 | mod.configureDataAPI = function() 34 | { 35 | // Return table columns 36 | api.app.all(/^\/data\/(columns)\/?([a-z_0-9]+)?$/, (req, res) => { 37 | if (mod.perms && !lib.isFlag(mod.perms[req.params[1] || "*"], req.params[0])) { 38 | return res.status(403).send("not allowed"); 39 | } 40 | var options = api.getOptions(req); 41 | if (req.params[1]) { 42 | return res.json(db.getColumns(req.params[1], options)); 43 | } 44 | res.json(db.tables); 45 | }); 46 | 47 | // Return table keys 48 | api.app.all(/^\/data\/(keys)\/([a-z_0-9]+)$/, (req, res) => { 49 | if (mod.perms && !lib.isFlag(mod.perms[req.params[1]], req.params[0])) { 50 | return res.status(403).send("not allowed"); 51 | } 52 | var options = api.getOptions(req); 53 | res.json({ data: db.getKeys(req.params[1], options) }); 54 | }); 55 | 56 | // Basic operations on a table 57 | api.app.post(/^\/data\/(select|scan|search|list|get|add|put|update|del|incr|replace)\/([a-z_0-9]+)$/, (req, res) => { 58 | if (mod.perms && !lib.isFlag(mod.perms[req.params[1]], req.params[0])) return res.status(403).send("not allowed"); 59 | 60 | var options = api.getOptions(req); 61 | options.noscan = 0; 62 | 63 | if (!db.getColumns(req.params[1], options)) return api.sendReply(res, 404, "Unknown table"); 64 | 65 | switch (req.params[0]) { 66 | case "scan": 67 | var rows = []; 68 | db.scan(req.params[1], req.query, options, (row, next) => { 69 | rows.push(row); 70 | next(); 71 | }, (err) => { 72 | api.sendJSON(req, err, { count: rows.length, data: rows }); 73 | }); 74 | break; 75 | 76 | default: 77 | db[req.params[0]](req.params[1], req.query, options, (err, rows, info) => { 78 | api.sendJSON(req, err, api.getResultPage(req, options, rows, info)); 79 | }); 80 | } 81 | }); 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /examples/modules/bk_file.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var path = require('path'); 7 | var util = require('util'); 8 | var fs = require('fs'); 9 | var http = require('http'); 10 | var url = require('url'); 11 | var bkjs = require('backendjs'); 12 | var db = bkjs.db; 13 | var api = bkjs.api; 14 | var app = bkjs.app; 15 | var ipc = bkjs.ipc; 16 | var msg = bkjs.msg; 17 | var core = bkjs.core; 18 | var lib = bkjs.lib; 19 | var logger = bkjs.logger; 20 | 21 | var files = { 22 | name: "bk_file", 23 | }; 24 | module.exports = files; 25 | 26 | // Create API endpoints and routes 27 | files.configureWeb = function(options, callback) 28 | { 29 | this.configureFilesAPI(); 30 | callback() 31 | } 32 | 33 | // Generic file management 34 | files.configureFilesAPI = function() 35 | { 36 | var self = this; 37 | 38 | api.app.all(/^\/file\/([a-z]+)$/, function(req, res) { 39 | var options = api.getOptions(req); 40 | 41 | if (!req.query.name) return api.sendReply(res, 400, "name is required"); 42 | if (!req.query.prefix) return api.sendReply(res, 400, "prefix is required"); 43 | var file = req.query.prefix.replace("/", "") + "/" + req.query.name.replace("/", ""); 44 | if (options.tm) file += options.tm; 45 | 46 | switch (req.params[0]) { 47 | case "get": 48 | api.getFile(req, file, options); 49 | break; 50 | 51 | case "add": 52 | case "put": 53 | options.name = req.query.name; 54 | options.prefix = req.query.prefix; 55 | api.putFile(req, req.query._name || "data", options, function(err) { 56 | api.sendReply(res, err); 57 | }); 58 | break; 59 | 60 | case "del": 61 | api.delFile(file, options, function(err) { 62 | api.sendReply(res, err); 63 | }); 64 | break; 65 | 66 | default: 67 | api.sendReply(res, 400, "Invalid command"); 68 | } 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /examples/modules/bk_system.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var bkjs = require('backendjs'); 7 | var api = bkjs.api; 8 | var ipc = bkjs.ipc; 9 | var core = bkjs.core; 10 | var lib = bkjs.lib; 11 | var logger = bkjs.logger; 12 | 13 | // System management 14 | const mod = { 15 | name: "bk_system", 16 | args: [ 17 | { name: "perms", type: "map", maptype: "list", descr: "Allowed operations, ex: -bk_system-perms restart:api,init:queue;config;db" }, 18 | ], 19 | }; 20 | module.exports = mod; 21 | 22 | // Create API endpoints and routes 23 | mod.configureWeb = function(options, callback) 24 | { 25 | this.configureSystemAPI(); 26 | callback() 27 | } 28 | 29 | // API for internal provisioning and configuration 30 | mod.configureSystemAPI = function() 31 | { 32 | api.app.post(/^\/system\/([^/]+)\/?(.+)?/, (req, res) => { 33 | if (mod.perms && !lib.isFlag(mod.perms[req.params[0]], req.params[1] || "*")) { 34 | return res.status(403).send("not allowed"); 35 | } 36 | 37 | var options = api.getOptions(req); 38 | switch (req.params[0]) { 39 | case "restart": 40 | ipc.sendMsg(`${req.params[1] || "api"}:restart`); 41 | res.json({}); 42 | break; 43 | 44 | case "init": 45 | if (req.params[1]) { 46 | ipc.broadcast(core.name + ":master", req.params[1] + ":" + req.params[0]); 47 | } 48 | res.json({}); 49 | break; 50 | 51 | case "params": 52 | var args = [ [ '', core.args ] ]; 53 | Object.keys(core.modules).forEach((n) => { 54 | if (core.modules[n].args) args.push([n, core.modules[n].args]); 55 | }); 56 | switch (req.params[1]) { 57 | case 'get': 58 | res.json(args.reduce((data, x) => { 59 | x[1].forEach((y) => { 60 | if (!y._name) return; 61 | var val = lib.objGet(x[0] ? core.modules[x[0]] : core, y._name); 62 | if (val == null && !options.total) return; 63 | data[y._key] = typeof val == "undefined" ? null : val; 64 | }); 65 | return data; 66 | }, { "-home": core.home, "-log": logger.level })); 67 | break; 68 | 69 | case "info": 70 | res.json(args.reduce((data, x) => { 71 | x[1].forEach((y) => { 72 | data[(x[0] ? x[0] + "-" : "") + y.name] = y; 73 | }); 74 | return data; 75 | }, {})); 76 | break; 77 | default: 78 | api.sendReply(res, 400, "Invalid command"); 79 | } 80 | break; 81 | 82 | default: 83 | api.sendReply(res, 400, "Invalid command"); 84 | } 85 | }); 86 | } 87 | 88 | -------------------------------------------------------------------------------- /examples/modules/dbp_mysql.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var util = require('util'); 7 | var core = require(__dirname + '/../lib/core'); 8 | var lib = require(__dirname + '/../lib/lib'); 9 | var db = require(__dirname + '/../lib/db'); 10 | var logger = require(__dirname + '/../lib/logger'); 11 | 12 | var pool = { 13 | name: "mysql", 14 | configOptions: { 15 | typesMap: { json: "text", bigint: "bigint", now: "bigint" }, 16 | sqlPlaceholder: "?", 17 | defaultType: "VARCHAR(128)", 18 | noIfExists: 1, 19 | noMultiSQL: 1 20 | }, 21 | createPool: function(options) { return new Pool(options); } 22 | }; 23 | module.exports = pool; 24 | 25 | db.modules.push(pool); 26 | 27 | function Pool(options) 28 | { 29 | var bkmysql = require("bkjs-mysql"); 30 | if (!bkmysql.Database) { 31 | logger.error("MySQL driver is not installed or compiled properly, consider to install libmysqlclient library"); 32 | return; 33 | } 34 | options.type = pool.name; 35 | db.SqlPool.call(this, options); 36 | this.configOptions = lib.objMerge(this.configOptions, pool.configOptions); 37 | } 38 | util.inherits(Pool, db.SqlPool); 39 | 40 | Pool.prototype.open = function(callback) 41 | { 42 | if (this.url == "default") this.url = "mysql:///" + db.dbName; 43 | var bkmysql = require("bkjs-mysql"); 44 | new bkmysql.Database(this.url, function(err) { 45 | callback(err, this); 46 | }); 47 | } 48 | 49 | Pool.prototype.close = function(client, callback) 50 | { 51 | client.close(callback); 52 | } 53 | 54 | Pool.prototype.cacheIndexes = function(options, callback) 55 | { 56 | var self = this; 57 | this.acquire(function(err, client) { 58 | if (err) return callback ? callback(err, []) : null; 59 | 60 | client.query("SHOW TABLES", function(err, tables) { 61 | lib.forEachSeries(tables, function(table, next) { 62 | table = table[Object.keys(table)[0]].toLowerCase(); 63 | client.query("SHOW INDEX FROM " + table, function(err, rows) { 64 | self.dbkeys = {}; 65 | self.dbindexes = {}; 66 | for (var i = 0; i < rows.length; i++) { 67 | if (!self.dbcolumns[table]) continue; 68 | var col = self.dbcolumns[table][rows[i].Column_name]; 69 | switch (rows[i].Key_name) { 70 | case "PRIMARY": 71 | if (!self.dbkeys[table]) self.dbkeys[table] = []; 72 | self.dbkeys[table].push(rows[i].Column_name); 73 | if (col) col.primary = true; 74 | break; 75 | 76 | default: 77 | if (!self.dbindexes[rows[i].Key_name]) self.dbindexes[rows[i].Key_name] = []; 78 | self.dbindexes[rows[i].Key_name].push(rows[i].Column_name); 79 | break; 80 | } 81 | } 82 | next(); 83 | }); 84 | }, function(err) { 85 | self.release(client); 86 | if (callback) callback(err); 87 | }); 88 | }); 89 | }); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /examples/modules/dynamodbstreams.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const logger = require(__dirname + '/../logger'); 7 | const lib = require(__dirname + '/../lib'); 8 | const aws = require(__dirname + '/../aws'); 9 | 10 | aws.queryDDBStreams = function(action, obj, options, callback) 11 | { 12 | this._queryDDB("DynamoDBStreams_20120810", "streams.dynamodb", action, obj, options, callback); 13 | } 14 | 15 | aws.ddbListStreams = function(query, options, callback) 16 | { 17 | if (typeof options == "function") callback = options, options = null; 18 | if (typeof query == "string") query = { table: query }; 19 | var q = { 20 | ExclusiveStartStreamArn: query.streamArn || query.StreamArn, 21 | TableName: query.table || query.TableName, 22 | Limit: query.limit || query.Limit || (options && options.count) || 100, 23 | }; 24 | var rc = { Streams: [] } 25 | lib.doWhilst( 26 | function(next) { 27 | aws.queryDDBStreams('ListStreams', q, options, function(err, res) { 28 | logger.debug("ddbstreams:", "ddbListStreams:", err, query, res); 29 | if (!err) { 30 | q.ExclusiveStartStreamArn = res.LastEvaluatedStreamArn; 31 | rc.Streams.push.apply(rc.Streams, res.Streams); 32 | } 33 | next(err); 34 | }); 35 | }, 36 | function() { 37 | return q.ExclusiveStartStreamArn; 38 | }, 39 | function(err) { 40 | if (typeof callback == "function") callback(err, rc); 41 | }); 42 | } 43 | 44 | aws.ddbDescribeStream = function(query, options, callback) 45 | { 46 | if (typeof options == "function") callback = options, options = null; 47 | if (typeof query == "string") query = { streamArn: query }; 48 | var q = { 49 | StreamArn: query.streamArn || query.StreamArn, 50 | ExclusiveStartShardId: query.shardId || query.ShardId || query.ExclusiveStartShardId, 51 | Limit: query.limit || query.Limit || (options && options.count) || 100, 52 | }; 53 | var rc = { Shards: [] }; 54 | lib.doWhilst( 55 | function(next) { 56 | aws.queryDDBStreams('DescribeStream', q, options, function(err, res) { 57 | logger.debug("ddbstreams:", "ddbDescribeStream:", err, query, res); 58 | if (!err) { 59 | q.ExclusiveStartShardId = res.StreamDescription.LastEvaluatedShardId; 60 | for (const p in res.StreamDescription) if (!rc[p]) rc[p] = res.StreamDescription[p]; 61 | rc.Shards.push.apply(rc.Shards, res.StreamDescription.Shards); 62 | } 63 | next(err); 64 | }); 65 | }, 66 | function() { 67 | return q.ExclusiveStartShardId; 68 | }, 69 | function(err) { 70 | if (typeof callback == "function") callback(err, rc); 71 | }); 72 | } 73 | 74 | aws.ddbGetShardIterator = function(query, options, callback) 75 | { 76 | if (typeof options == "function") callback = options, options = null; 77 | var q = { 78 | StreamArn: query.streamArn || query.StreamArn, 79 | ShardId: query.shardId || query.ShardId, 80 | ShardIteratorType: query.latest ? "LATEST" : 81 | query.oldest ? "TRIM_HORIZON" : 82 | query.at ? "AT_SEQUENCE_NUMBER" : 83 | query.after ? "AFTER_SEQUENCE_NUMBER" : 84 | query.type || query.ShardIteratorType || 85 | options.shardIteratorType || "TRIM_HORIZON", 86 | SequenceNumber: query.sequence || query.SequenceNumber, 87 | }; 88 | aws.queryDDBStreams('GetShardIterator', q, options, function(err, res) { 89 | logger.debug("ddbstreams:", "ddbGetShardIterator:", err, query, res); 90 | if (typeof callback == "function") callback(err, res); 91 | }); 92 | } 93 | 94 | aws.ddbGetShardRecords = function(query, options, callback) 95 | { 96 | if (typeof options == "function") callback = options, options = null; 97 | var q = { 98 | ShardIterator: query.shardIterator || query.ShardIterator || query.NextShardIterator, 99 | Limit: query.limit || query.Limit || (options && options.count) || 1000, 100 | }; 101 | aws.queryDDBStreams('GetRecords', q, options, function(err, res) { 102 | logger.debug("ddbstreams:", "ddbGetShardRecords:", err, lib.arrayLength(res.Records), "records", res.NextShardIterator); 103 | if (!err) { 104 | for (var i in res.Records) { 105 | if (res.Records[i].dynamodb.Keys) res.Records[i].dynamodb.Keys = aws.fromDynamoDB(res.Records[i].dynamodb.Keys); 106 | if (res.Records[i].dynamodb.OldImage) res.Records[i].dynamodb.OldImage = aws.fromDynamoDB(res.Records[i].dynamodb.OldImage); 107 | if (res.Records[i].dynamodb.NewImage) res.Records[i].dynamodb.NewImage = aws.fromDynamoDB(res.Records[i].dynamodb.NewImage); 108 | } 109 | } 110 | if (typeof callback == "function") callback(err, res); 111 | }); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /examples/modules/ipcc_memcache.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var util = require('util'); 7 | var logger = require(__dirname + '/../lib/logger'); 8 | var lib = require(__dirname + '/../lib/lib'); 9 | var ipc = require(__dirname + "/../lib/ipc"); 10 | var Client = require(__dirname + "/../lib/ipc_client"); 11 | 12 | // Cache client based on Memcached server using https://github.com/3rd-Eden/node-memcached 13 | // 14 | // To support more than one server use either one: 15 | // 16 | // ipc-cache=memcache://host1?bk-servers=host2,host3 17 | // 18 | // ipc-cache-memcache=memcache://host1 19 | // ipc-cache-memcache-options-servers=host1,host2 20 | // 21 | // To pass memcached module specific options: 22 | // 23 | // ipc-cache-options-failures=5 24 | // ipc-cache-options-maxValue=1024 25 | // 26 | 27 | var client = { 28 | name: "memcache", 29 | }; 30 | module.exports = client; 31 | 32 | ipc.modules.push(client); 33 | 34 | client.createClient = function(url, options) 35 | { 36 | if (/^memcache:/.test(url)) return new IpcMemcacheClient(url, options); 37 | } 38 | 39 | function IpcMemcacheClient(url, options) 40 | { 41 | Client.call(this, url, options); 42 | this.options.servers = lib.strSplitUnique(this.options.servers); 43 | var h = (this.hostname || "127.0.0.1") + ":" + (this.port || this.options.port || 11211); 44 | if (this.options.servers.indexOf(h) == -1) this.options.servers.unshift(h); 45 | if (typeof this.options.idle != "number") this.options.idle = 900000; 46 | 47 | var Memcached = require("memcached"); 48 | this.client = new Memcached(this.options.servers, this.options); 49 | this.client.on("error", (err) => { 50 | logger.error("memcache:", this.url, err); 51 | this.emit("error", err); 52 | }); 53 | this.client.on("ready", this.emit.bind(this, "ready")); 54 | } 55 | util.inherits(IpcMemcacheClient, Client); 56 | 57 | IpcMemcacheClient.prototype.close = function() 58 | { 59 | Client.prototype.close.call(this); 60 | this.client.end(); 61 | } 62 | 63 | IpcMemcacheClient.prototype.stats = function(options, callback) 64 | { 65 | this.client.stats(callback); 66 | } 67 | 68 | IpcMemcacheClient.prototype.clear = function(pattern, callback) 69 | { 70 | this.client.flush(callback || lib.noop); 71 | } 72 | 73 | IpcMemcacheClient.prototype.get = function(key, options, callback) 74 | { 75 | if (Array.isArray(key)) { 76 | this.client.getMulti(key, function(err, data) { 77 | if (!err) data = key.map(function(x) { return data[x] }); 78 | lib.tryCall(callback, err, data); 79 | }); 80 | } else { 81 | var self = this; 82 | this.client.get(key, function(err, data) { 83 | if (typeof data == "undefined" && options && options.set) { 84 | var ttl = options && lib.isNumber(options.ttl) ? options.ttl : lib.isNumber(this.options.ttl) ? this.options.ttl : 0; 85 | self.client.add(key, options.set, ttl > 0 ? Math.ceil(ttl/1000) : 0); 86 | } 87 | lib.tryCall(callback, err, data); 88 | }) 89 | } 90 | } 91 | 92 | IpcMemcacheClient.prototype.put = function(key, val, options, callback) 93 | { 94 | var ttl = options && lib.isNumber(options.ttl) ? options.ttl : lib.isNumber(this.options.ttl) ? this.options.ttl : 0; 95 | this.client.set(key, val, ttl > 0 ? Math.ceil(ttl/1000) : 0, callback || lib.noop); 96 | } 97 | 98 | IpcMemcacheClient.prototype.incr = function(key, val, options, callback) 99 | { 100 | var self = this; 101 | var ttl = options && lib.isNumber(options.ttl) ? options.ttl : lib.isNumber(this.options.ttl) ? this.options.ttl : 0; 102 | this.client.incr(key, val, function(err, data) { 103 | if (!err && ttl > 0) self.client.touch(key, Math.ceil(ttl/1000)); 104 | lib.tryCall(callback, err, data); 105 | }) 106 | } 107 | 108 | IpcMemcacheClient.prototype.del = function(key, options, callback) 109 | { 110 | this.client.del(key, callback || lib.noop); 111 | } 112 | 113 | -------------------------------------------------------------------------------- /examples/modules/msg_sns.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var logger = require(__dirname + '/../lib/logger'); 7 | var core = require(__dirname + '/../lib/core'); 8 | var lib = require(__dirname + '/../lib/lib'); 9 | var aws = require(__dirname + '/../lib/aws'); 10 | var msg = require(__dirname + '/../lib/msg'); 11 | 12 | var client = { 13 | name: "sns", 14 | }; 15 | module.exports = client; 16 | 17 | msg.modules.push(client); 18 | 19 | client.check = function(dev) 20 | { 21 | return dev.urn.match(/^arn:aws:sns:/); 22 | } 23 | 24 | client.init = function(options) 25 | { 26 | } 27 | 28 | client.close = function(callback) 29 | { 30 | callback(); 31 | } 32 | 33 | // Send push notification to a device using AWS SNS service, device_id must be a valid SNS endpoint ARN. 34 | // 35 | // The options may contain the following properties: 36 | // - msg - message text 37 | // - badge - badge number 38 | // - type - set type of the packet 39 | // - id - send id in the user properties 40 | client.send = function(dev, options, callback) 41 | { 42 | if (!dev.id) return lib.tryCall(callback, lib.newError("invalid device:" + dev)); 43 | 44 | var pkt = {}; 45 | // Format according to the rules per platform 46 | if (dev.id.match("/APNS/")) { 47 | pkt = { aps: { alert: {} } }; 48 | if (options.category) pkt.aps.category = options.category; 49 | if (options.contentAvailable) pkt.aps["content-available"] = 1; 50 | if (options.badge) pkt.aps.badge = lib.toNumber(options.badge); 51 | if (options.sound) pkt.aps.sound = typeof options.sound == "string" ? options.sound : "default"; 52 | 53 | if (options.msg) pkt.aps.alert.body = options.msg; 54 | if (options.title) pkt.aps.alert.title = options.title; 55 | if (options.launchImage) pkt.aps.alert["launch-image"] = options.launchImage; 56 | if (options.locKey) pkt.aps.alert["loc-key"] = options.locKey; 57 | if (options.locArgs) pkt.aps.alert["loc-args"] = options.locArgs; 58 | if (options.titleLocKey) pkt.aps.alert["title-loc-key"] = options.titleLocKey; 59 | if (options.titleLocArgs) pkt.aps.alert["title-loc-args"] = options.titleLocArgs; 60 | if (options.actionLocKey) pkt.aps.alert["action-loc-args"] = options.actionLocKey; 61 | if (options.alertAction) pkt.aps.alert.action = options.alertAction; 62 | if (Object.keys(pkt.aps.alert).length == 1 && options.msg) pkt.aps.alert = options.msg; 63 | 64 | if (options.id) pkt.id = options.id; 65 | if (options.type) pkt.type = options.type; 66 | if (options.url) pkt.url = options.url; 67 | if (options.user_id) pkt.user_id = options.user_id; 68 | pkt = { APNS: lib.stringify(pkt) }; 69 | } else 70 | if (dev.id.match("/GCM/")) { 71 | pkt = { data: {}, notification: {} }; 72 | if (options.name) pkt.collapse_key = options.name; 73 | if (options.ttl) pkt.time_to_live = lib.toNumber(options.ttl); 74 | if (options.delay) pkt.delay_while_idle = lib.toBool(options.delay); 75 | 76 | if (options.id) pkt.data.url = String(options.url); 77 | if (options.url) pkt.data.url = String(options.url); 78 | if (options.type) pkt.data.type = String(options.type); 79 | if (options.user_id) pkt.data.user_id = options.user_id; 80 | 81 | if (options.msg) pkt.data.msg = options.msg; 82 | if (options.badge) pkt.data.badge = lib.toBool(options.badge); 83 | if (options.sound) pkt.data.sound = lib.toBool(options.sound); 84 | if (options.vibrate) pkt.data.vibrate = lib.toBool(options.vibrate); 85 | pkt = { GCM: lib.stringify(pkt) }; 86 | } 87 | aws.snsPublish(dev.id, pkt, callback); 88 | return true; 89 | } 90 | -------------------------------------------------------------------------------- /examples/srp/README.md: -------------------------------------------------------------------------------- 1 | # Backendjs application 2 | 3 | 1. To install 4 | 5 | npm install 6 | 7 | 2. Run the app 8 | 9 | ./app.sh 10 | 11 | 4. Point browser to http://localhost:8000 12 | 13 | -------------------------------------------------------------------------------- /examples/srp/app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec node app.js -watch $(pwd) -watch $(pwd) -web -log debug -etc-dir $(pwd)/etc -web-path $(pwd)/web $@ 4 | 5 | -------------------------------------------------------------------------------- /examples/srp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "author": "backendjs", 4 | "name": "app", 5 | "description": "The app template", 6 | "main": "app.js", 7 | "dependencies": { 8 | "backendjs": ">=0.10.0" 9 | }, 10 | "engines": { "node": ">= 6.11.0" }, 11 | "scripts": { "start": "node app.js" }, 12 | "license": "BSD-3-Clause" 13 | } 14 | -------------------------------------------------------------------------------- /examples/srp/test.js: -------------------------------------------------------------------------------- 1 | 2 | // Vectors from https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol 3 | tests.test_srp = function(callback, test) 4 | { 5 | var user = lib.getArg("-user", 'andré@example.org'); 6 | var secret = lib.getArg("-secret") || Buffer.from('00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d', "hex"); 7 | var salt = lib.getArg("-salt", '00f1000000000000000000000000000000000000000000000000000000000179'); 8 | var a = lib.getArg("-a", "f2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d3d7"); 9 | var b = lib.getArg("-b", "f3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f"); 10 | var k = lib.getArg("-k", "5b9e8ef059c6b32ea59fc1d322d37f04aa30bae5aa9003b8321e21ddb04e300"); 11 | var v = lib.getArg("-v", "173ffa0263e63ccfd6791b8ee2a40f048ec94cd95aa8a3125726f9805e0c8283c658dc0b607fbb25db68e68e93f2658483049c68af7e8214c49fde2712a775b63e545160d64b00189a86708c69657da7a1678eda0cd79f86b8560ebdb1ffc221db360eab901d643a75bf1205070a5791230ae56466b8c3c1eb656e19b794f1ea0d2a077b3a755350208ea0118fec8c4b2ec344a05c66ae1449b32609ca7189451c259d65bd15b34d8729afdb5faff8af1f3437bbdc0c3d0b069a8ab2a959c90c5a43d42082c77490f3afcc10ef5648625c0605cdaace6c6fdc9e9a7e6635d619f50af7734522470502cab26a52a198f5b00a279858916507b0b4e9ef9524d6"); 12 | var B = lib.getArg("-B", "22ce5a7b9d81277172caa20b0f1efb4643b3becc53566473959b07b790d3c3f08650d5531c19ad30ebb67bdb481d1d9cf61bf272f8439848fdda58a4e6abc5abb2ac496da5098d5cbf90e29b4b110e4e2c033c70af73925fa37457ee13ea3e8fde4ab516dff1c2ae8e57a6b264fb9db637eeeae9b5e43dfaba9b329d3b8770ce89888709e026270e474eef822436e6397562f284778673a1a7bc12b6883d1c21fbc27ffb3dbeb85efda279a69a19414969113f10451603065f0a012666645651dde44a52f4d8de113e2131321df1bf4369d2585364f9e536c39a4dce33221be57d50ddccb4384e3612bbfd03a268a36e4f7e01de651401e108cc247db50392"); 13 | var A = lib.getArg("-A", "7da76cb7e77af5ab61f334dbd5a958513afcdf0f47ab99271fc5f7860fe2132e5802ca79d2e5c064bb80a38ee08771c98a937696698d878d78571568c98a1c40cc6e7cb101988a2f9ba3d65679027d4d9068cb8aad6ebff0101bab6d52b5fdfa81d2ed48bba119d4ecdb7f3f478bd236d5749f2275e9484f2d0a9259d05e49d78a23dd26c60bfba04fd346e5146469a8c3f010a627be81c58ded1caaef2363635a45f97ca0d895cc92ace1d09a99d6beb6b0dc0829535c857a419e834db12864cd6ee8a843563b0240520ff0195735cd9d316842d5d3f8ef7209a0bb4b54ad7374d73e79be2c3975632de562c596470bb27bad79c3e2fcddf194e1666cb9fc"); 14 | var x = lib.getArg("-x", "b5200337cc3f3f926cdddae0b2d31029c069936a844aff58779a545be89d0abe"); 15 | var K = lib.getArg("-K", "e68fd0112bfa31dcffc8e9c96a1cbadb4c3145978ff35c73e5bf8d30bbc7499a"); 16 | var M = lib.getArg("-M", "27949ec1e0f1625633436865edb037e23eb6bf5cb91873f2a2729373c2039008"); 17 | var S = lib.getArg("-S", "92aaf0f527906aa5e8601f5d707907a03137e1b601e04b5a1deb02a981f4be037b39829a27dba50f1b27545ff2e28729c2b79dcbdd32c9d6b20d340affab91a626a8075806c26fe39df91d0ad979f9b2ee8aad1bc783e7097407b63bfe58d9118b9b0b2a7c5c4cdebaf8e9a460f4bf6247b0da34b760a59fac891757ddedcaf08eed823b090586c63009b2d740cc9f5397be89a2c32cdcfe6d6251ce11e44e6ecbdd9b6d93f30e90896d2527564c7eb9ff70aa91acc0bac1740a11cd184ffb989554ab58117c2196b353d70c356160100ef5f4c28d19f6e59ea2508e8e8aac6001497c27f362edbafb25e0f045bfdf9fb02db9c908f10340a639fe84c31b27"); 18 | var u = lib.getArg("-u", "b284aa1064e8775150da6b5e2147b47ca7df505bed94a6f4bb2ad873332ad732"); 19 | 20 | auth.srp.init(); 21 | logger.info("user:", user, secret, "salt:", salt, "k:", auth.srp.k.toString(16) == k); 22 | 23 | var r = auth.srp.verifier(user, secret, salt); 24 | logger.info("r:", r, "v:", r[1] == v, "x:", r[2] == x); 25 | 26 | var c1 = auth.srp.client1(a) 27 | logger.info("c1:", c1, "A:", c1[1] == A); 28 | 29 | var s1 = auth.srp.server1(r[1], b); 30 | logger.info("s1:", s1, "B:", s1[1] == B) 31 | 32 | var c2 = auth.srp.client2(user, secret, r[0], c1[0], s1[1]) 33 | logger.info("c2:", c2, "K:", c2[0] == K, "M:", c2[1] == M, "S:", c2[2] == S, "u:", c2[3] == u, "x:", c2[4] == x, "A:", c2[5] == A); 34 | 35 | var s2 = auth.srp.server2(user, r[1], s1[0], c1[1], c2[1]) 36 | logger.info("s2:", s2, "S:", s2[1] == S, "u:", s2[2] == u) 37 | 38 | var c3 = auth.srp.client3(c1[1], c2[1], c2[0], s2[0]) 39 | logger.info("c3:", c3) 40 | 41 | callback(); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /examples/srp/web/bkjs-srp.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * backend.js SRP client 3 | * Vlad Seryakov vseryakov@gmail.com 2021 4 | */ 5 | 6 | bkjs.srp = { 7 | hexN: 'AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310D' + 8 | 'CD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF74' + 9 | '7359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D' + 10 | '5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB6' + 11 | '94B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73', 12 | hexG: '02', 13 | BigInteger: window.BigInteger, 14 | 15 | init: function() { 16 | if (!this._) { 17 | this.N = this.toInt(this.hexN); 18 | this.g = this.toInt(this.hexG); 19 | this.k = this.hash(this.N, this.g); 20 | this._ = 1; 21 | } 22 | }, 23 | 24 | toInt: function(n) { 25 | return n instanceof this.BigInteger ? n : typeof n == "string" ? new this.BigInteger(n, 16) : this.rand(); 26 | }, 27 | 28 | toBin: function(...args) { 29 | var h = []; 30 | for (const i in args) { 31 | if (args[i] instanceof this.BigInteger) { 32 | h = bkjs.crypto.bconcat(h, bkjs.crypto.hex2bin(args[i].toString(16).padStart(512, "0"))); 33 | } else 34 | if (typeof args[i] == "string") { 35 | h = bkjs.crypto.bconcat(h, bkjs.crypto.utf82bin(args[i])); 36 | } else { 37 | h = bkjs.crypto.bconcat(h, args[i]); 38 | } 39 | } 40 | return h; 41 | }, 42 | 43 | hash: function(...args) { 44 | return new this.BigInteger(bkjs.crypto.sha256(this.toBin.apply(this, args), "hex"), 16); 45 | }, 46 | 47 | rand: function() { 48 | return new this.BigInteger(bkjs.random(32), 16); 49 | }, 50 | 51 | x: function(user, secret, salt) { 52 | return this.hash(bkjs.crypto.hex2bin(this.toInt(salt).toString(16).padStart(64, "0")), bkjs.crypto.sha256(this.toBin(user, ':', secret))); 53 | }, 54 | 55 | verifier: function(user, secret, salt) { 56 | this.init(); 57 | const s = this.toInt(salt); 58 | const x = this.x(user, secret, s); 59 | const v = this.g.modPow(x, this.N); 60 | return [s.toString(16), v.toString(16), x.toString(16)]; 61 | }, 62 | 63 | client1: function(salt) { 64 | this.init(); 65 | const a = this.toInt(salt); 66 | const A = this.g.modPow(a, this.N); 67 | return [a.toString(16), A.toString(16)]; 68 | }, 69 | 70 | client2: function(user, secret, salt, a, B) { 71 | this.init(); 72 | B = this.toInt(B); 73 | if (B.mod(this.N).toString() == "0") return null; 74 | a = this.toInt(a); 75 | const x = this.x(user, secret, salt); 76 | const A = this.g.modPow(a, this.N); 77 | const u = this.hash(A, B); 78 | const S = B.subtract(this.k.multiply(this.g.modPow(x, this.N))).modPow(a.add(u.multiply(x)), this.N).mod(this.N); 79 | const K = this.hash(S); 80 | const M = this.hash(A, B, S); 81 | return [K.toString(16), M.toString(16), S.toString(16), u.toString(16), x.toString(16), A.toString(16)]; 82 | }, 83 | 84 | client3: function(A, M1, K, M2) { 85 | const M = this.hash(this.toInt(A), this.toInt(M1), this.toInt(K)); 86 | return [M.equals(this.toInt(M2)), M.toString(16)]; 87 | }, 88 | } 89 | -------------------------------------------------------------------------------- /examples/srp/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backend SRP6a API 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | 66 | 69 | 70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 |
78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /lib/api/csrf.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | const api = require(__dirname + '/../api'); 8 | const logger = require(__dirname + '/../logger'); 9 | 10 | // CSRF token format: TYPE,RANDOM_INT,EXPIRE_MS,[UID] 11 | // 12 | // `type`` is `h` for header or `c`` for cookie 13 | // 14 | // Implements double cookie protection using HTTP and cookie tokens, both must be present. 15 | // 16 | // In addition a token may contain the user id which must be the same as logged in user. 17 | // 18 | 19 | // Return HTTP CSRF token, can be used in templates or forms, the cookie token will reuse the same token 20 | api.getCsrfToken = function(req) 21 | { 22 | if (req && !req.csrfToken) { 23 | req._csrfToken = `,${lib.randomInt()},${Date.now() + this.csrfAge},${!req._csrfPub && req.user?.id || ""}`; 24 | req.csrfToken = lib.encrypt(this.csrfTokenSecret || this.accessTokenSecret, "h" + req._csrfToken); 25 | logger.debug("getCsrfToken:", "new", req.options, "T:", req._csrfToken); 26 | } 27 | return req?.csrfToken; 28 | } 29 | 30 | // Returns .ok == false if CSRF token verification fails, both header and cookie are checked and retuned as .h and .c 31 | api.verifyCsrfToken = function(req) 32 | { 33 | var secret = this.csrfTokenSecret || this.accessTokenSecret; 34 | var ok, h = req.headers[this.csrfHeaderName] || req.query[this.csrfHeaderName] || req.body[this.csrfHeaderName]; 35 | h = lib.decrypt(secret, h).split(","); 36 | if (h[0] === "h" && lib.toNumber(h[2]) > Date.now() && (!h[3] || h[3] === req.user?.id)) { 37 | var c = lib.decrypt(secret, req.cookies[this.csrfHeaderName]).split(","); 38 | if (c[0] === "c" && lib.toNumber(c[2]) > Date.now() && (!c[3] || c[3] === req.user?.id)) { 39 | // When using many tabs tokens may get out of sync but both must be valid user tokens 40 | ok = h[1] === c[1] || (req.user?.id && req.user?.id === h[3] && h[3] === c[3]); 41 | } 42 | } 43 | return { ok, h, c }; 44 | } 45 | 46 | // For configured endpoints check for a token and fail if not present or invalid 47 | api.checkCsrfToken = function(req, options) 48 | { 49 | if (lib.testRegexpObj(req.options.path, this.csrfCheckPath)) { 50 | if (options?.force || !lib.testRegexp(req.method, this.csrfSkipMethod)) { 51 | var t = this.verifyCsrfToken(req); 52 | if (!t.ok) { 53 | logger.debug("invalidCsrfToken:", req.options, "H:", t.h, "C:", t.c, "HDR:", req.headers, "Q:", req.query); 54 | return { status: 401, message: this.errInvalidCsrf, code: "NOCSRF" }; 55 | } 56 | logger.debug("checkCsrfToken:", "ok", req.options, "H:", t.h, "C:", t.c); 57 | } 58 | } else { 59 | var set = lib.testRegexpObj(req.options.path, this.csrfSetPath); 60 | if (!set) { 61 | // Set public tokens if no valid tokens are present 62 | if (!lib.testRegexpObj(req.options.path, this.csrfPubPath)) return; 63 | if (this.verifyCsrfToken(req).ok) return; 64 | req._csrfPub = 1; 65 | } 66 | } 67 | 68 | // Set header/cookie at the time of sending HTTP headers so user id is included in the token if present. 69 | this.registerPreHeaders(req, (req, res, status) => { 70 | if (req.csrfToken == 0 || lib.testRegexp(status, api.csrfSkipStatus)) return; 71 | 72 | res.header(api.csrfHeaderName, api.getCsrfToken(req)); 73 | var csrfToken = lib.encrypt(api.csrfTokenSecret || api.accessTokenSecret, "c" + req._csrfToken); 74 | var opts = api.makeSessionCookie(req, { 75 | httpOnly: true, 76 | maxAge: api.csrfAge, 77 | secure: api.sessionSecure, 78 | sameSite: api.sessionSameSite, 79 | }); 80 | res.cookie(api.csrfHeaderName, csrfToken, opts); 81 | }); 82 | } 83 | 84 | // Do not return CSRF token in cooies or headers 85 | api.skipCsrfToken = function(req) 86 | { 87 | req.csrfToken = 0; 88 | } 89 | 90 | // Reset CSRF tokens from cookies and headers 91 | api.clearCsrfToken = function(req) 92 | { 93 | if (!req?.res) return; 94 | api.skipCsrfToken(req); 95 | var opts = api.makeSessionCookie(req, { 96 | expires: new Date(1), 97 | httpOnly: true, 98 | sameSite: "strict", 99 | secure: true 100 | }); 101 | req.res.cookie(api.csrfHeaderName, "", opts); 102 | req.res.header(api.csrfHeaderName, req.csrfToken); 103 | } 104 | 105 | -------------------------------------------------------------------------------- /lib/api/session.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | const api = require(__dirname + '/../api'); 8 | const logger = require(__dirname + '/../logger'); 9 | const cache = require(__dirname + '/../cache'); 10 | 11 | // Find a closest cookie by host/domain/path, longest takes precedence, returns found cookie merged with the options 12 | api.makeSessionCookie = function(req, options) 13 | { 14 | if (!req._sessionCookie) { 15 | var path = "", host = ""; 16 | for (const p in this.sessionCookie) { 17 | if (p[0] == "/") { 18 | if (req.options.path.startsWith(p) && p.length > path.length) { 19 | path = p; 20 | } 21 | } else 22 | if ((p === req.options.host || p === req.options.domain) && p.length > host.length) { 23 | host = p; 24 | } 25 | } 26 | if (path) req._sessionCookie = Object.assign({}, this.sessionCookie[path]); 27 | if (host) req._sessionCookie = Object.assign(req._sessionCookie || {}, this.sessionCookie[host]); 28 | } 29 | return Object.assign(options || {}, req._sessionCookie); 30 | } 31 | 32 | // Return named encrypted cookie 33 | api.getSessionCookie = function(req, name) 34 | { 35 | var value = req.cookies && req.cookies[name]; 36 | return value && lib.base64ToJson(value, this.accessTokenSecret); 37 | } 38 | 39 | // Set a cookie by name and domain, the value is always encrypted 40 | api.setSessionCookie = function(req, name, value) 41 | { 42 | if (!req?.res || !name) return ""; 43 | value = value ? lib.jsonToBase64(value, this.accessTokenSecret) : ""; 44 | var opts = api.makeSessionCookie(req, { 45 | path: "/", 46 | httpOnly: true, 47 | secure: this.sessionSecure, 48 | sameSite: this.sessionSameSite, 49 | }); 50 | if (value) { 51 | opts.maxAge = this.sessionAge; 52 | } else { 53 | opts.expires = new Date(1); 54 | } 55 | req.res.cookie(name, value, opts); 56 | } 57 | 58 | // Setup session cookies or access token for automatic authentication without signing, req must be complete with all required 59 | // properties after successful authorization. 60 | api.handleSessionSignature = function(req, callback) 61 | { 62 | var options = this.getOptions(req); 63 | options.session = options.session && req.user?.login && req.user?.secret && req.headers ? true : false; 64 | var hooks = this.findHook('sig', req.method, req.path); 65 | logger.debug("handleSessionSignature:", hooks.length, "hooks", options); 66 | 67 | if (!hooks.length) { 68 | if (options.session) this.createSessionSignature(req, options); 69 | return lib.tryCall(callback); 70 | } 71 | 72 | lib.forEachSeries(hooks, function(hook, next) { 73 | hook.callback.call(api, req, req.user, null, next); 74 | }, (sig) => { 75 | if (!sig) { 76 | if (options.session) this.createSessionSignature(req, options); 77 | } 78 | lib.tryCall(callback); 79 | }, true); 80 | } 81 | 82 | api.createSessionSignature = function(req, options) 83 | { 84 | var sig = this.createSignature(req.user.login, req.user.secret, "", req.headers.host, "", { version: 2, expires: options?.sessionAge || this.sessionAge }); 85 | if (!this.noSession) this.setSessionCookie(req, this.signatureHeaderName, sig[this.signatureHeaderName]); 86 | return sig; 87 | } 88 | 89 | api.clearSessionSignature = function(req) 90 | { 91 | this.saveSessionSignature(req.signature, -Date.now()); 92 | 93 | if (!this.noSession) { 94 | this.setSessionCookie(req, this.signatureHeaderName, ""); 95 | } 96 | } 97 | 98 | api.checkSessionSignature = function(sig, callback) 99 | { 100 | if (this.noSession || !this.sessionAge || !sig?.signature) return lib.tryCall(callback); 101 | cache.get(`SIG:${sig.login}:${sig.signature}`, { cacheName: this.sessionCache }, (err, val) => { 102 | logger.debug("checkSessionSignature:", sig, "VAL:", val); 103 | lib.tryCall(callback, err, val); 104 | }); 105 | } 106 | 107 | api.saveSessionSignature = function(sig, val, callback) 108 | { 109 | if (typeof val == "function") callback = val, val = 0; 110 | if (this.noSession || !this.sessionAge || !sig?.signature) return lib.tryCall(callback); 111 | logger.debug("saveSessionSignature:", sig, "VAL:", val); 112 | cache.put(`SIG:${sig.login}:${sig.signature}`, val || Date.now(), { cacheName: this.sessionCache, ttl: this.sessionAge }, callback); 113 | } 114 | -------------------------------------------------------------------------------- /lib/api/users.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const api = require(__dirname + '/../api'); 7 | const core = require(__dirname + '/../core'); 8 | const lib = require(__dirname + '/../lib'); 9 | const users = require(__dirname + '/../users'); 10 | 11 | // User management 12 | // 13 | // use -api.users-cap-noweb 1 to disable default endpoints 14 | 15 | const mod = { 16 | name: "api.users", 17 | args: [ 18 | { name: "err-(.+)", descr: "Error messages for various cases" }, 19 | { name: "cap-(.+)", type: "int", strip: "cap-", descr: "Capability parameters" }, 20 | ], 21 | noweb: 0, 22 | sigversion: -1, 23 | 24 | errInvalidLogin: "No username or password provided", 25 | }; 26 | module.exports = mod; 27 | 28 | mod.configureWeb = function(options, callback) 29 | { 30 | if (this.noweb) return callback(); 31 | 32 | // Authentication check with signature/session 33 | api.app.post(/^\/auth$/, (req, res) => { 34 | if (!req.user?.id) { 35 | return api.sendReply(res, { status: 417, message: mod.errInvalidLogin, code: "NOLOGIN" }); 36 | } 37 | api.handleSessionSignature(req, () => { 38 | req.options.cleanup = users.table; 39 | req.options.cleanup_strict = 1; 40 | api.sendJSON(req, null, req.user); 41 | }); 42 | }); 43 | 44 | // Login with just the secret without signature 45 | api.app.post(/^\/login$/, (req, res) => { 46 | if (!req.query.login || !req.query.secret) { 47 | return api.sendReply(res, { status: 417, message: mod.errInvalidLogin, code: "NOLOGIN" }); 48 | } 49 | // Create internal signature from the login data 50 | req.signature = api.newSignature(req, "version", mod.sigversion, "source", "l", "login", req.query.login, "secret", req.query.secret); 51 | delete req.query.login; 52 | delete req.query.secret; 53 | 54 | api.checkAuthentication(req, (err) => { 55 | if (!req.user?.id) { 56 | return api.sendJSON(req, err || { status: 417, message: mod.errInvalidLogin, code: "NOLOGIN" }); 57 | } 58 | api.handleSessionSignature(req, () => { 59 | req.options.cleanup = users.table; 60 | req.options.cleanup_strict = 1; 61 | api.sendJSON(req, null, req.user); 62 | }); 63 | }); 64 | }); 65 | 66 | // Clear sessions and access tokens 67 | api.app.post(/^\/logout$/, (req, res) => { 68 | api.handleLogout(req); 69 | api.sendJSON(req); 70 | }); 71 | 72 | callback(); 73 | } 74 | 75 | mod.command = function(op, query, options, callback) 76 | { 77 | if (typeof options == "function") callback = options, options = null; 78 | 79 | var req = { stopOnError: 1, query, options }; 80 | 81 | lib.series([ 82 | function(next) { 83 | core.runMethods(`bkPrepare${op}User`, req, next); 84 | }, 85 | function(next) { 86 | users[op.toLowerCase()](query, options, (err, row) => { 87 | if (!err) req.user = row; 88 | next(err); 89 | }); 90 | }, 91 | function(next) { 92 | delete req.stopOnError; 93 | core.runMethods(`bk${op}User`, req, next); 94 | }, 95 | ], callback, true); 96 | } 97 | 98 | mod.add = function(query, options, callback) 99 | { 100 | return mod.command("Add", query, options, callback); 101 | } 102 | 103 | mod.update = function(query, options, callback) 104 | { 105 | return mod.command("Update", query, options, callback); 106 | } 107 | 108 | mod.del = function(query, options, callback) 109 | { 110 | return mod.command("Del", query, options, callback); 111 | } 112 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | // This is a skeleton module to be extended by the specific application logic. It provides all 7 | // callbacks and hooks that are called by the core backend modules 8 | // during different phases, like initialization, shutting down, etc... 9 | // 10 | // It should be used for custom functions and methods to be defined, the `app` module is always available. 11 | // 12 | // All app modules in the modules/ subdirectory use the same prototype, i.e. all hooks are available for custom app modules as well. 13 | // 14 | const app = { 15 | name: "app", 16 | args: [], 17 | }; 18 | 19 | module.exports = app; 20 | 21 | // Called after all config files are loaded and command line args are parsed, home directory is set but before the db is initialized, 22 | // the primary purpose of this early call is to setup environment before connecting to the database. This is called regardless of the server 23 | // to be started and intended to initialize the common environment before the database and other subsystems are initialized. 24 | app.configure = function(options, callback) { callback(); } 25 | 26 | // Called after the core.init has been initialized successfully, this can be redefined in the applications to add additional 27 | // init steps that all processes require to have. All database pools and other confugration is ready at this point. This hook is 28 | // called regardless of what kind of server is about to start, it is always called before starting a server or shell. 29 | app.configureModule = function(options, callback) { callback(); } 30 | 31 | // This handler is called during the Express server initialization just after the security middleware. 32 | // 33 | // NOTE: `api.app` refers to the Express instance. 34 | app.configureMiddleware = function(options, callback) { callback(); }; 35 | 36 | // This handler is called after the Express server has been setup and all default API endpoints initialized but the Web server 37 | // is not ready for incoming requests yet. This handler can setup additional API endpoints, add/modify table descriptions. 38 | // 39 | // NOTE: `api.app` refers to the Express instance 40 | app.configureWeb = function(options, callback) { callback(); }; 41 | 42 | // This handler is called during the Web server startup to create additional servers like websocket in addition to the default HTTP(s) servers 43 | app.configureServer = function(options, callback) { callback(); } 44 | 45 | // Perform shutdown sequence when a Web process is about to exit 46 | // 47 | // NOTE: `api.app` refers to the Express instance 48 | app.shutdownWeb = function(options, callback) { callback(); } 49 | 50 | // This handler is called during the master server startup, this is the process that monitors the worker jobs and performs jobs scheduling 51 | app.configureMaster = function(options, callback) { callback(); } 52 | 53 | // This handler is called on job worker instance startup after the tables are intialized and it is ready to process the job 54 | app.configureWorker = function(options, callback) { callback(); } 55 | 56 | // Perform last minute operations inside a worker process before exit, the callback must be called eventually which will exit the process. 57 | // This method can be overrided to implement custom worker shutdown procedure in order to finish pending tasks like network calls. 58 | app.shutdownWorker = function(options, callback) { callback(); } 59 | 60 | // This callback is called by the shell process to setup additional command or to execute a command which is not 61 | // supported by the standard shell. Setting options.done to 1 will stop the shell, this is a signal that command has already 62 | // been processed. 63 | app.configureShell = function(options, callback) { callback(); } 64 | -------------------------------------------------------------------------------- /lib/aws/ecs.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | var logger = require(__dirname + '/../logger'); 7 | var lib = require(__dirname + '/../lib'); 8 | var aws = require(__dirname + '/../aws'); 9 | 10 | // AWS ECS API request 11 | aws.queryECS = function(action, obj, options, callback) 12 | { 13 | this.queryService("ecs", "AmazonEC2ContainerServiceV20141113", action, obj, options, callback); 14 | } 15 | 16 | aws.ecsDescribeTasks = function(options, callback) 17 | { 18 | var req = { 19 | cluster: options.cluster || this.ecsCluster, 20 | tasks: lib.strSplit(options.tasks), 21 | include: ["TAGS"], 22 | }; 23 | aws.queryECS("DescribeTasks", req, (err, rc) => { 24 | for (const i in rc.tasks) aws.ecsPrepareTask(rc.tasks[i]); 25 | lib.tryCall(callback, err, rc); 26 | }); 27 | } 28 | 29 | aws.ecsPrepareTask = function(task) 30 | { 31 | task.id = task.taskArn.split("/").pop(); 32 | task.name = task.containers[0].name; 33 | var attrs = lib.isArray(task.attributes, []); 34 | for (const a of attrs) { 35 | if (a.name == 'ecs.cpu-architecture') task.arch = a.value; 36 | } 37 | for (const i in task.attachments) { 38 | if (task.attachments[i].type == "ElasticNetworkInterface") { 39 | var details = lib.isArray(task.attachments[i].details, []); 40 | for (const d of details) { 41 | if (d.name == 'privateIPv4Address') task.privateIpAddress = d.value; else 42 | if (d.name == 'subnetId') task.subnetId = d.value; 43 | } 44 | 45 | } 46 | } 47 | if (task.group && task.group.startsWith("family:")) { 48 | task.family = task.group.substr(7); 49 | } else { 50 | task.family = task.taskDefinitionArn.split(/[:/]/).at(-2); 51 | } 52 | return task; 53 | } 54 | 55 | aws.ecsRunTask = function(options, callback) 56 | { 57 | var req = { 58 | taskDefinition: options.task, 59 | count: options.count || 1, 60 | cluster: options.cluster || this.ecsCluster, 61 | clientToken: options.clientToken, 62 | enableExecuteCommand: options.enableExecuteCommand, 63 | enableECSManagedTags: options.enableECSManagedTags, 64 | group: options.group, 65 | launchType: options.launchType, 66 | capacityProviderStrategy: options.provider ? [{ capacityProvider: options.provider }] : options.capacityProviderStrategy, 67 | networkConfiguration: options.networkConfiguration, 68 | platformVersion: options.platformVersion, 69 | propagateTags: options.propagateTags, 70 | referenceId: options.referenceId, 71 | startedBy: options.startedBy, 72 | tags: options.tags, 73 | placementStrategy: options.placementStrategy, 74 | placementConstraints: options.placementConstraints, 75 | volumeConfigurations: options.volumeConfigurations, 76 | }; 77 | 78 | var network = {}; 79 | if (options.publicIp || this.publicIp) { 80 | network.assignPublicIp = "ENABLED"; 81 | } 82 | if (options.groupId || this.groupId) { 83 | network.securityGroups = lib.strSplitUnique(options.groupId || this.groupId); 84 | } 85 | if (options.subnetId || this.subnetId) { 86 | network.subnets = lib.strSplitUnique(options.subnetId || this.subnetId); 87 | } 88 | if (!lib.isEmpty(network)) { 89 | req.networkConfiguration = { awsvpcConfiguration: network }; 90 | } 91 | 92 | var overrides = {}; 93 | if (options.cpu) { 94 | overrides.cpu = String(options.cpu); 95 | } 96 | if (options.memory) { 97 | overrides.memory = String(options.memory); 98 | } 99 | if (options.disk) { 100 | overrides.ephemeralStorage = { sizeInGiB: options.disk }; 101 | } 102 | if (options.role) { 103 | overrides.taskRoleArn = options.role; 104 | } 105 | if (options.execRole) { 106 | overrides.executionRoleArn = options.execRole; 107 | } 108 | 109 | if (options.container) { 110 | var co = { name: options.container }; 111 | overrides.containerOverrides = [co]; 112 | if (options.env) { 113 | co.environment = []; 114 | for (const p in options.env) { 115 | co.environment.push({ name: p, value: options.env[p] }); 116 | } 117 | } 118 | if (lib.isArray(options.files)) { 119 | co.environmentFiles = options.files.map((x) => ({ type: "s3", value: x })); 120 | } 121 | if (options.cpu) { 122 | co.cpu = lib.toNumber(options.cpu); 123 | } 124 | if (options.memory) { 125 | co.memory = lib.toNumber(options.memory); 126 | } 127 | } 128 | if (!lib.isEmpty(overrides)) { 129 | req.overrides = overrides; 130 | } 131 | 132 | logger.debug('eccRunTask:', this.name, req, "OPTS:", options); 133 | this.queryECS("RunTask", req, options, callback); 134 | } 135 | 136 | 137 | -------------------------------------------------------------------------------- /lib/aws/other.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const url = require('url'); 7 | const path = require('path'); 8 | const fs = require('fs'); 9 | const core = require(__dirname + '/../core'); 10 | const lib = require(__dirname + '/../lib'); 11 | const aws = require(__dirname + '/../aws'); 12 | 13 | // Assume a role and return new credentials that can be used in other API calls 14 | aws.stsAssumeRole = function(options, callback) 15 | { 16 | var params = { 17 | RoleSessionName: options.name || core.name, 18 | RoleArn: options.role, 19 | }; 20 | this.querySTS("AssumeRole", params, options, (err, obj) => { 21 | if (!err) { 22 | obj = lib.objGet(obj, "AssumeRoleResponse.AssumeRoleResult"); 23 | obj.credentials = { 24 | key: obj.Credentials.AccessKeyId, 25 | secret: obj.Credentials.SecretAccessKey, 26 | token: obj.Credentials.SessionToken, 27 | expiration: lib.toDate(obj.Credentials.Expiration).getTime(), 28 | }; 29 | delete obj.Credentials; 30 | } 31 | if (typeof callback == "function") callback(err, obj); 32 | }); 33 | } 34 | 35 | // Detect image featires using AWS Rekognition service, the `name` can be a Buffer, a local file or an url to the S3 bucket. In the latter case 36 | // the url can be just apath to the file inside a bucket if `options.bucket` is specified, otherwise it must be a public S3 url with the bucket name 37 | // to be the first part of the host name. For CDN/CloudFront cases use the `option.bucket` option. 38 | aws.detectLabels = function(name, options, callback) 39 | { 40 | if (typeof options == "function") callback = options, options = null; 41 | 42 | if (Buffer.isBuffer(name)) { 43 | const req = { 44 | Image: { 45 | Bytes: name.toString("base64") 46 | } 47 | }; 48 | aws.queryRekognition("DetectLabels", req, options, callback); 49 | } else 50 | if (name && options && options.bucket) { 51 | const req = { 52 | Image: { 53 | S3Object: { 54 | Bucket: options.bucket, 55 | Name: name[0] == "/" ? name.substr(1) : name 56 | } 57 | } 58 | }; 59 | aws.queryRekognition("DetectLabels", req, options, callback); 60 | } else 61 | if (name && name[0] == "/") { 62 | fs.readFile(path.join(core.path.images, path.normalize(name)), function(err, data) { 63 | if (err) return callback && callback(err); 64 | const req = { 65 | Image: { 66 | Bytes: data.toString("base64") 67 | } 68 | }; 69 | aws.queryRekognition("DetectLabels", req, options, callback); 70 | }); 71 | } else { 72 | name = url.parse(String(name)); 73 | if (name.pathname && name.pathname[0] == "/") name.pathname = name.pathname.substr(1); 74 | const req = { 75 | Image: { 76 | S3Object: { 77 | Bucket: name.hostname && name.hostname.split(".")[0], 78 | Name: name.pathname 79 | } 80 | } 81 | }; 82 | if (!req.Image.S3Object.Bucket || !req.Image.S3Object.Name) return callback && callback({ status: 404, message: "invalid image" }); 83 | aws.queryRekognition("DetectLabels", req, options, callback); 84 | } 85 | } 86 | 87 | // Return a list of certificates, 88 | // - `status` can limit which certs to return, PENDING_VALIDATION | ISSUED | INACTIVE | EXPIRED | VALIDATION_TIMED_OUT | REVOKED | FAILED 89 | aws.listCertificates = function(options, callback) 90 | { 91 | var token, list = []; 92 | 93 | lib.doWhilst( 94 | function(next) { 95 | aws.queryACM("ListCertificates", { CertificateStatuses: options.status, MaxItems: 1000, NextToken: token }, (err, rc) => { 96 | if (err) return next(err); 97 | token = rc.NextToken; 98 | for (const i in rc.CertificateSummaryList) { 99 | list.push(rc.CertificateSummaryList[i]); 100 | } 101 | next(); 102 | }); 103 | }, 104 | function() { 105 | return token; 106 | }, 107 | function(err) { 108 | lib.tryCall(callback, err, list); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /lib/aws/ses.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const core = require(__dirname + '/../core'); 7 | const lib = require(__dirname + '/../lib'); 8 | const aws = require(__dirname + '/../aws'); 9 | const logger = require(__dirname + '/../logger'); 10 | 11 | // AWS SES API request 12 | aws.querySES = function(action, obj, options, callback) 13 | { 14 | this.queryEndpoint("email", '2010-12-01', action, obj, options, callback); 15 | } 16 | 17 | // Send an email via SES 18 | // The following options supported: 19 | // - from - an email to use in the From: header 20 | // - cc - list of email to use in CC: header 21 | // - bcc - list of emails to use in Bcc: header 22 | // - replyTo - list of emails to ue in ReplyTo: header 23 | // - returnPath - email where to send bounces 24 | // - charset - charset to use, default is UTF-8 25 | // - html - if set the body is sent as MIME HTML 26 | aws.sesSendEmail = function(to, subject, body, options, callback) 27 | { 28 | if (typeof options == "function") callback = options, options = null; 29 | if (!options) options = lib.empty; 30 | 31 | var params = { "Message.Subject.Data": subject, "Message.Subject.Charset": options.charset || "UTF-8" }; 32 | params["Message.Body." + (options.html ? "Html" : "Text") + ".Data"] = body; 33 | params["Message.Body." + (options.html ? "Html" : "Text") + ".Charset"] = options.charset || "UTF-8"; 34 | params.Source = options.from || core.emailFrom || ("admin@" + core.domain); 35 | lib.strSplit(to).forEach((x, i) => { params["Destination.ToAddresses.member." + (i + 1)] = x; }) 36 | if (options.cc) lib.strSplit(options.cc).forEach((x, i) => { params["Destination.CcAddresses.member." + (i + 1)] = x; }) 37 | if (options.bcc) lib.strSplit(options.bcc).forEach((x, i) => { params["Destination.BccAddresses.member." + (i + 1)] = x; }) 38 | if (options.replyTo) lib.strSplit(options.replyTo).forEach((x, i) => { params["ReplyToAddresses.member." + (i + 1)] = x; }) 39 | if (options.returnPath) params.ReturnPath = options.returnPath; 40 | this.querySES("SendEmail", params, options, callback); 41 | } 42 | 43 | // Send raw email 44 | // The following options accepted: 45 | // - to - list of email addresses to use in RCPT TO 46 | // - from - an email to use in from header 47 | aws.sesSendRawEmail = function(body, options, callback) 48 | { 49 | if (typeof options == "function") callback = options, options = null; 50 | 51 | var params = { "RawMessage.Data": body }; 52 | if (options) { 53 | if (options.from) params.Source = options.from; 54 | if (options.to) lib.strSplit(options.to).forEach((x, i) => { params["Destinations.member." + (i + 1)] = x; }) 55 | } 56 | this.querySES("SendRawEmail", params, options, callback); 57 | } 58 | 59 | // SES V2 version 60 | aws.sesSendRawEmail2 = function(body, options, callback) 61 | { 62 | if (typeof options == "function") callback = options, options = null; 63 | var params = { 64 | Content: { Raw: { Data: body } } 65 | }; 66 | if (options) { 67 | if (options.from) params.FromEmailAddress = options.from; 68 | if (options.to) params.Destination = { ToAddresses: lib.strSplit(options.to) } 69 | } 70 | 71 | var headers = { 'content-type': 'application/x-amz-json-1.1' }; 72 | var opts = this.queryOptions("POST", lib.stringify(params), headers, options); 73 | opts.region = this.getServiceRegion("email", options?.region || this.region || 'us-east-1'); 74 | opts.endpoint = "ses"; 75 | opts.action = "SendEmail"; 76 | opts.signer = this.querySigner; 77 | logger.debug(opts.action, opts); 78 | this.httpGet(`https://email.${opts.region}.amazonaws.com/v2/email/outbound-emails`, opts, (err, params) => { 79 | if (params.status != 200) err = aws.parseError(params, options); 80 | if (typeof callback == "function") callback(err, params.obj); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /lib/aws/sqs.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | const aws = require(__dirname + '/../aws'); 8 | 9 | // AWS SQS API request 10 | aws.querySQS = function(action, obj, options, callback) 11 | { 12 | this.queryEndpoint("sqs", '2012-11-05', action, obj, options, callback); 13 | } 14 | 15 | // Receive message(s) from the SQS queue, the callback will receive a list with messages if no error. 16 | // The following options can be specified: 17 | // - count - how many messages to receive 18 | // - timeout - how long to wait, in milliseconds, this is for Long Poll 19 | // - visibilityTimeout - the duration (in milliseconds) that the received messages are hidden from subsequent retrieve requests 20 | // - attempt - request attempt id for FIFO queues 21 | // after being retrieved by a ReceiveMessage request. 22 | aws.sqsReceiveMessage = function(url, options, callback) 23 | { 24 | if (typeof options == "function") callback = options, options = null; 25 | 26 | var params = { QueueUrl: url }; 27 | if (options) { 28 | if (options.count) params.MaxNumberOfMessages = options.count; 29 | if (options.visibilityTimeout > 999) params.VisibilityTimeout = Math.round(options.visibilityTimeout/1000); 30 | if (options.timeout > 999) params.WaitTimeSeconds = Math.round(options.timeout/1000); 31 | if (options.attempt) params.ReceiveRequestAttemptId = options.attempt; 32 | } 33 | this.querySQS("ReceiveMessage", params, options, function(err, obj) { 34 | var rows = []; 35 | if (!err) rows = lib.objGet(obj, "ReceiveMessageResponse.ReceiveMessageResult.Message", { list: 1 }); 36 | if (typeof callback == "function") callback(err, rows); 37 | }); 38 | } 39 | 40 | // Send a message to the SQS queue. 41 | // The options can specify the following: 42 | // - delay - how long to delay this message in milliseconds 43 | // - group - a group id for FIFO queues 44 | // - unique - deduplication id for FIFO queues 45 | // - attrs - an object with additional message attributes to send, use only string, numbers or binary values, 46 | // all other types will be converted into strings 47 | aws.sqsSendMessage = function(url, body, options, callback) 48 | { 49 | if (typeof options == "function") callback = options, options = null; 50 | 51 | var params = { QueueUrl: url, MessageBody: body }; 52 | if (options) { 53 | if (options.delay > 999) params.DelaySeconds = Math.round(options.delay/1000); 54 | if (options.group) params.MessageGroupId = options.group; 55 | if (options.unique) params.MessageDeduplicationId = options.unique; 56 | if (options.attrs) { 57 | var n = 1; 58 | for (var p in options.attrs) { 59 | var type = typeof options.attrs[p] == "number" ? "Number" : typeof options.attrs[p] == "string" ? "String" : "Binary"; 60 | params["MessageAttribute." + n + ".Name"] = p; 61 | params["MessageAttribute." + n + ".Value." + type + "Value"] = options.attrs[p]; 62 | params["MessageAttribute." + n + ".Value.DataType"] = type; 63 | n++; 64 | } 65 | } 66 | } 67 | this.querySQS("SendMessage", params, options, function(err, obj) { 68 | var rows = []; 69 | if (!err) rows = lib.objGet(obj, "ReceiveMessageResponse.ReceiveMessageResult.Message", { list: 1 }); 70 | if (typeof callback == "function") callback(err, rows); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /lib/cache/client.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const url = require('url'); 7 | const { EventEmitter } = require("events"); 8 | const logger = require(__dirname + '/../logger'); 9 | const lib = require(__dirname + '/../lib'); 10 | const metrics = require(__dirname + '/../metrics'); 11 | 12 | // Base class for the cache clients, implements cache protocol in the same class, 13 | // not supported methods just do nothing without raising any errors 14 | 15 | class Client extends EventEmitter { 16 | name = "cache client" 17 | cacheName = "" 18 | options = {} 19 | 20 | constructor(options) { 21 | super(); 22 | this.setMaxListeners(0); 23 | this.url = String(options?.url || ""); 24 | this.metrics = new metrics.Timer(); 25 | this.applyOptions(options); 26 | this.on("ready", () => { this.ready = true }); 27 | logger.debug("client:", this.url, this.options); 28 | } 29 | 30 | // Close current connection, ports.... not valid after this call 31 | close() { 32 | this.url = ""; 33 | this.options = {}; 34 | this.metrics.end(); 35 | this.removeAllListeners(); 36 | } 37 | 38 | // Prepare options to be used safely, parse the reserved params from the url 39 | applyOptions(options) { 40 | for (const p in options) { 41 | if (p[0] != "_" && p != "url") this.options[p] = options[p]; 42 | } 43 | const h = url.parse(this.url, true); 44 | this.port = h.port || 0; 45 | this.protocol = h.protocol; 46 | this.hostname = h.hostname || ""; 47 | this.pathname = h.pathname || ""; 48 | for (const p in h.query) { 49 | var d = p.match(/^bk-(.+)/); 50 | if (!d) continue; 51 | this.options[d[1]] = lib.isNumeric(h.query[p]) ? lib.toNumber(h.query[p]) : h.query[p]; 52 | delete h.query[p]; 53 | } 54 | h.search = null; 55 | h.path = null; 56 | this.url = url.format(h); 57 | } 58 | 59 | // Handle reserved options 60 | applyReservedOptions(options) {} 61 | 62 | // Returns the cache statistics to the callback as the forst argument, the object tructure is specific to each implementstion 63 | stats(options, callback) { 64 | lib.tryCall(callback); 65 | } 66 | 67 | // CACHE MANAGEMENT 68 | 69 | // Clear all or only matched keys from the cache 70 | clear(pattern, callback) { 71 | lib.tryCall(callback); 72 | } 73 | 74 | // Returns an item from the cache by a key, callback is required and it acceptes only the item, 75 | // on any error null or undefined will be returned 76 | get(key, options, callback) { 77 | lib.tryCall(callback); 78 | } 79 | 80 | // Store an item in the cache, `options.ttl` can be used to specify TTL in milliseconds 81 | put(key, val, options, callback) { 82 | lib.tryCall(callback); 83 | } 84 | 85 | // Add/substract a number from the an item, returns new number in the callback if provided, in case of an error null/indefined should be returned 86 | incr(key, val, options, callback) { 87 | lib.tryCall(callback, null, 0); 88 | } 89 | 90 | // Delete an item from the cache 91 | del(key, options, callback) { 92 | lib.tryCall(callback); 93 | } 94 | 95 | // LOCKING MANAGEMENT 96 | 97 | // By default return an error 98 | lock(name, options, callback) { 99 | logger.error("lock:", "NOT IMPLEMENTED", this.name, name, options); 100 | lib.tryCall(callback, { status: 500, message: "not implemented" }); 101 | } 102 | 103 | unlock(name, options, callback) { 104 | lib.tryCall(callback); 105 | } 106 | 107 | // RATE CONTROL 108 | 109 | // Rate limit check, by default it uses the master LRU cache meaning it works within one physical machine only. 110 | // 111 | // The options must have the following properties: 112 | // - name - unique id, can be IP address, account id, etc... 113 | // - rate, max, interval - same as for `metrics.TokenBucket` rate limiter. 114 | // 115 | // The callback arguments must be: 116 | // - 1st arg is a delay to wait till the bucket is ready again, 117 | // - 2nd arg is an object with the bucket state: { delay:, count:, total:, elapsed: } 118 | // 119 | limiter(options, callback) { 120 | logger.error("limiter:", "NOT IMPLEMENTED", this.name, options); 121 | lib.tryCall(callback, 60000, {}); 122 | } 123 | 124 | } 125 | 126 | module.exports = Client; 127 | 128 | -------------------------------------------------------------------------------- /lib/cache/local.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2024 4 | // 5 | 6 | const core = require("../core"); 7 | const Client = require(__dirname + "/client"); 8 | 9 | // Client that uses the local process or master process for jobs. 10 | 11 | const client = { 12 | name: "local", 13 | 14 | create: function(options) { 15 | if (/^local:/.test(options?.url)) return new LocalClient(options); 16 | } 17 | }; 18 | module.exports = client; 19 | 20 | class LocalClient extends Client { 21 | 22 | constructor(options) { 23 | super(options); 24 | this.name = client.name; 25 | this.applyOptions(); 26 | this.emit("ready"); 27 | } 28 | 29 | limiter(options, callback) { 30 | var opts = { 31 | name: options.name, 32 | rate: options.rate, 33 | max: options.max, 34 | interval: options.interval, 35 | expire: options.ttl > 0 ? Date.now() + options.ttl : 0, 36 | reset: options.reset, 37 | multiplier: options.multiplier, 38 | cacheName: this.cacheName, 39 | }; 40 | const msg = core.modules.cache.localLimiter(opts); 41 | callback(msg.consumed ? 0 : msg.delay, msg); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /lib/cache/worker.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2024 4 | // 5 | 6 | const core = require("../core"); 7 | const Client = require(__dirname + "/client"); 8 | 9 | // Client that uses master process rate limiter and workers for jobs. 10 | 11 | const client = { 12 | name: "worker", 13 | 14 | create: function(options) { 15 | if (/^worker:/.test(options?.url)) return new WorkerClient(options); 16 | } 17 | }; 18 | module.exports = client; 19 | 20 | class WorkerClient extends Client { 21 | 22 | constructor(options) { 23 | super(options); 24 | this.name = client.name; 25 | this.applyOptions(); 26 | this.emit("ready"); 27 | } 28 | 29 | limiter(options, callback) { 30 | var opts = { 31 | name: options.name, 32 | rate: options.rate, 33 | max: options.max, 34 | interval: options.interval, 35 | expire: options.ttl > 0 ? Date.now() + options.ttl : 0, 36 | reset: options.reset, 37 | multiplier: options.multiplier, 38 | cacheName: this.cacheName, 39 | }; 40 | core.modules.ipc.sendMsg("ipc:limiter", opts, (msg) => { 41 | callback(msg.consumed ? 0 : msg.delay, msg); 42 | }); 43 | } 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /lib/core/sendgrid.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const core = require(__dirname + '/../core'); 7 | const lib = require(__dirname + '/../lib'); 8 | const logger = require(__dirname + '/../logger'); 9 | 10 | function SendGridTransport(options) 11 | { 12 | this.options = options || {}; 13 | this.name = 'SendGrid'; 14 | this.version = core.vesion; 15 | 16 | } 17 | core.SendGridTransport = SendGridTransport; 18 | 19 | SendGridTransport.prototype.send = function(mail, callback) 20 | { 21 | mail.normalize((err, data) => { 22 | if (err) return lib.tryCall(callback, err); 23 | 24 | var req = { 25 | from: { email: data.from?.address, name: data.from?.name }, 26 | subject: data.subject, 27 | personalizations: [{}], 28 | }; 29 | if (lib.isArray(data.to)) { 30 | const to = data.to.filter((x) => (x.address)).map((x) => ({ email: x.address, name: x.name })); 31 | if (to.length) req.personalizations[0].to = to; 32 | } 33 | if (lib.isArray(data.cc)) { 34 | const cc = data.cc.filter((x) => (x.address)).map((x) => ({ email: x.address, name: x.name })); 35 | if (cc.length) req.personalizations[0].cc = cc; 36 | } 37 | if (lib.isArray(data.bcc)) { 38 | const bcc = data.bcc.filter((x) => (x.address)).map((x) => ({ email: x.address, name: x.name })); 39 | if (bcc.length) req.personalizations[0].bcc = bcc; 40 | } 41 | 42 | if (lib.isArray(data.replyTo)) { 43 | req.reply_to_list = data.replyTo.filter((x) => (x.address)).map((x) => ({ email: x.address, name: x.name })); 44 | } 45 | if (data.text) { 46 | if (!req.content) req.content = []; 47 | req.content.push({ type: "text/plain", value: data.text }); 48 | } 49 | if (data.html) { 50 | if (!req.content) req.content = []; 51 | req.content.push({ type: "text/html", value: data.html }); 52 | } 53 | if (lib.isArray(data.attachments)) { 54 | for (const a of data.attachments) { 55 | if (!a.content) continue; 56 | if (a.encoding != "base64") a.content = Buffer.from(a.content).toString('base64'); 57 | if (!req.attachments) req.attachments = []; 58 | req.attachments.push({ type: a.contentType, content: a.content, filename: a.filename, disposition: a.disposition, content_id: a.cid }); 59 | } 60 | } 61 | if (data.sendgrid) { 62 | for (const p of ["template_id", "categories", "headers", "custom_args", "batch_id", "asm", "ip_pool_name", "mail_settings", "tracking_settings"]) { 63 | if (data.sendgrid[p]) req[p] = data.sendgrid[p]; 64 | } 65 | } 66 | if (data.dryrun) return lib.tryCall(callback, null, { data: data, req: req }); 67 | 68 | core.httpGet("https://api.sendgrid.com/v3/mail/send", 69 | { headers: { 70 | Authorization: `Bearer ${this.options.apikey || core.sendgridKey}`, 71 | "content-type": "application/json", 72 | }, 73 | method: "POST", 74 | postdata: req, 75 | retryOnError: function() { return this.status == 429 || this.status >= 500 }, 76 | retryCount: this.options.retryCount || 3, 77 | retryTimeout: this.options.retryTimeout || 5000, 78 | }, (err, rc) => { 79 | if (!err && rc.status >= 400) { 80 | err = { status: rc.status, message: rc.obj?.errors?.length && rc.obj.errors[0].message || rc.data }; 81 | } 82 | if (err) logger.error("sendmail:", this.name, err, this.options); 83 | lib.tryCall(callback, err, { messageId: data.messageId }); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /lib/db/pg.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const util = require('util'); 7 | const lib = require(__dirname + '/../lib'); 8 | const db = require(__dirname + '/../db'); 9 | const logger = require(__dirname + '/../logger'); 10 | 11 | const pool = { 12 | name: "pg", 13 | type: "pg", 14 | configOptions: { 15 | typesMap: { list: "text[]" }, 16 | noReplace: 1, 17 | noListOps: 0, 18 | noListTypes: 0, 19 | onConflictUpdate: 1, 20 | schema: ['public'], 21 | }, 22 | createPool: function(options) { 23 | return new Pool(options); 24 | } 25 | }; 26 | module.exports = pool; 27 | 28 | db.modules.push(pool); 29 | 30 | class PgClient { 31 | constructor(client) { 32 | this.pg = client; 33 | } 34 | 35 | query(text, values, options, callback) { 36 | if (typeof options == "function") callback = options, options = null; 37 | if (typeof values == "function") callback = values, values = null, options = null; 38 | this.pg.query(text, values, (err, result) => { 39 | callback(err, result?.rows || [], { affected_rows: result?.rowCount }); 40 | }); 41 | } 42 | } 43 | 44 | function Pool(options) 45 | { 46 | pool.pg = require("pg"); 47 | db.SqlPool.call(this, options, pool); 48 | } 49 | util.inherits(Pool, db.SqlPool) 50 | 51 | Pool.prototype.open = function(callback) 52 | { 53 | if (this.url == "default") this.url = "postgresql://postgres@127.0.0.1/" + db.dbName; 54 | const client = new pool.pg.Client(/:\/\//.test(this.url) ? { connectionString: this.url } : this.configOptions); 55 | client.connect((err) => { 56 | if (err) { 57 | logger.error('connect:', this.name, err); 58 | callback(err); 59 | } else { 60 | client.on('error', logger.error.bind(logger, this.name)); 61 | client.on('notice', logger.log.bind(logger, this.name)); 62 | client.on('notification', logger.info.bind(logger, this.name)); 63 | callback(err, new PgClient(client)); 64 | } 65 | }); 66 | } 67 | 68 | Pool.prototype.close = function(client, callback) 69 | { 70 | client.pg.end(callback); 71 | } 72 | 73 | // Cache indexes using the information_schema 74 | Pool.prototype.cacheIndexes = function(options, callback) 75 | { 76 | this.acquire((err, client) => { 77 | if (err) return callback(err, []); 78 | 79 | client.query("SELECT t.relname as table, i.relname as index, indisprimary as pk, array_agg(a.attname ORDER BY a.attnum) as cols "+ 80 | "FROM pg_class t, pg_class i, pg_index ix, pg_attribute a, pg_catalog.pg_namespace n "+ 81 | "WHERE t.oid = ix.indrelid and i.oid = ix.indexrelid and a.attrelid = t.oid and n.oid = t.relnamespace and " + 82 | " a.attnum = ANY(ix.indkey) and t.relkind = 'r' and n.nspname not in ('pg_catalog', 'pg_toast') " + 83 | (lib.isArray(options.tables) ? `AND t.relname IN (${db.sqlValueIn(options.tables)})` : "") + 84 | "GROUP BY t.relname, i.relname, ix.indisprimary ORDER BY t.relname, i.relname", (err, rows) => { 85 | if (err) logger.error('cacheIndexes:', self.name, err); 86 | this.dbkeys = {}; 87 | this.dbindexes = {}; 88 | for (const i in rows) { 89 | if (rows[i].pk) { 90 | this.dbkeys[rows[i].table] = rows[i].cols; 91 | } else { 92 | this.dbindexes[rows[i].index] = rows[i].cols; 93 | } 94 | } 95 | this.release(client); 96 | callback(err, []); 97 | }); 98 | }); 99 | } 100 | 101 | Pool.prototype.updateOps = function(req, op, name, value, placeholder) 102 | { 103 | switch (op) { 104 | case "add": 105 | // Add to a list 106 | return name + "=" + name + "||" + placeholder; 107 | 108 | case "del": 109 | // Delete from a list 110 | return name + "=array_remove(" + name + "," + placeholder + ")"; 111 | 112 | default: 113 | return db.SqlPool.prototype.updateOps.call(this, req, op, name, value, placeholder); 114 | } 115 | } 116 | 117 | Pool.prototype.bindValue = function(req, name, value, op) 118 | { 119 | // array_remove can only work with single value 120 | if (op == "del" && Array.isArray(value)) return value[0]; 121 | return value; 122 | } 123 | 124 | 125 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | exports.logger = require(__dirname + '/logger'); 7 | exports.lib = require(__dirname + '/lib'); 8 | exports.core = require(__dirname + '/core'); 9 | exports.pool = require(__dirname + '/pool'); 10 | exports.metrics = require(__dirname + '/metrics'); 11 | exports.cache = require(__dirname + '/cache'); 12 | exports.queue = require(__dirname + '/queue'); 13 | exports.ipc = require(__dirname + '/ipc'); 14 | exports.aws = require(__dirname + '/aws'); 15 | exports.db = require(__dirname + '/db'); 16 | exports.push = require(__dirname + '/push'); 17 | exports.server = require(__dirname + '/server'); 18 | exports.api = require(__dirname + '/api'); 19 | exports.users = require(__dirname + '/users'); 20 | exports.jobs = require(__dirname + '/jobs'); 21 | exports.events = require(__dirname + '/events'); 22 | exports.httpGet = require(__dirname + '/httpget'); 23 | exports.stats = require(__dirname + '/stats'); 24 | exports.logwatcher = require(__dirname + '/logwatcher'); 25 | exports.app = require(__dirname + '/app'); 26 | exports.shell = { name: "shell", help: [], cmdIndex: 1 }; 27 | for (const p in exports) if (p != "core") exports.core.addModule(exports[p]); 28 | 29 | exports.core.addModule(require(__dirname + "/api/users"), require(__dirname + "/api/passkeys"), require(__dirname + "/api/ws")) 30 | 31 | exports.modules = exports.core.modules; 32 | -------------------------------------------------------------------------------- /lib/lib/hash.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const Hashids = require("hashids/cjs"); 7 | const crypto = require('node:crypto'); 8 | const logger = require(__dirname + '/../logger'); 9 | const lib = require(__dirname + '/../lib'); 10 | 11 | // Hash and base64 encoded, default algorithm is sha1, uses node crypto module which is based on OLpenSSL 12 | lib.hash = function (data, algorithm, encode) 13 | { 14 | encode = encode === "binary" ? undefined : encode || "base64"; 15 | try { 16 | return crypto.createHash(algorithm || "sha1").update(data || "").digest(encode); 17 | } catch (e) { 18 | logger.error('hash:', algorithm, encode, e.stack); 19 | return ""; 20 | } 21 | } 22 | 23 | // Return cached Hashids object for the given configuration 24 | // Properties: 25 | // - salt - hashid salt, default is lib.salt 26 | // - min - minimum size of a hashid 27 | // - alphabet - chars allowed in hashids, default is lib.base32 28 | // - separators - hashid separator characters 29 | // - counter - max counter value to wrap back to 1, default is 65535 30 | lib.getHashid = function(options) 31 | { 32 | var min = options?.min || 0; 33 | var salt = options?.salt || this.salt; 34 | var alphabet = options?.alphabet || this.base62; 35 | var separators = options?.separators || ""; 36 | var key = salt + min + alphabet + separators; 37 | if (!this.hashids[key]) { 38 | this.hashids[key] = new Hashids(salt, lib.toNumber(min), alphabet, separators); 39 | this.hashids[key]._counter = lib.randomShort(); 40 | } 41 | if (++this.hashids[key]._counter > (options?.counter || 65535)) { 42 | this.hashids[key]._counter = 1; 43 | } 44 | return this.hashids[key]; 45 | } 46 | 47 | // 32-bit MurmurHash3 implemented by bryc (github.com/bryc) 48 | lib.murmurHash3 = function(key, seed = 0) 49 | { 50 | if (typeof key != "string") return 0; 51 | 52 | var k, p1 = 3432918353, p2 = 461845907, h = seed | 0; 53 | 54 | for (var i = 0, b = key.length & -4; i < b; i += 4) { 55 | k = key[i+3] << 24 | key[i+2] << 16 | key[i+1] << 8 | key[i]; 56 | k = Math.imul(k, p1); k = k << 15 | k >>> 17; 57 | h ^= Math.imul(k, p2); h = h << 13 | h >>> 19; 58 | h = Math.imul(h, 5) + 3864292196 | 0; 59 | } 60 | 61 | k = 0; 62 | switch (key.length & 3) { 63 | case 3: k ^= key[i+2] << 16; 64 | case 2: k ^= key[i+1] << 8; 65 | case 1: k ^= key[i]; 66 | k = Math.imul(k, p1); k = k << 15 | k >>> 17; 67 | h ^= Math.imul(k, p2); 68 | } 69 | 70 | h ^= key.length; 71 | h ^= h >>> 16; h = Math.imul(h, 2246822507); 72 | h ^= h >>> 13; h = Math.imul(h, 3266489909); 73 | h ^= h >>> 16; 74 | 75 | return h >>> 0; 76 | } 77 | -------------------------------------------------------------------------------- /lib/lib/lru.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | 8 | // Simple LRU cache in memory, supports get,put,del operations only, TTL can be specified in milliseconds as future time 9 | lib.LRUCache = function(max) 10 | { 11 | this.max = max || 10000; 12 | this.map = new Map(); 13 | this.head = {}; 14 | this.tail = this.head.next = { prev: this.head }; 15 | } 16 | 17 | lib.LRUCache.prototype.get = function(key) 18 | { 19 | const node = this.map.get(key); 20 | if (node == undefined) return; 21 | if (node.ttl && node.ttl < Date.now()) { 22 | this.del(key); 23 | return; 24 | } 25 | node.prev.next = node.next; 26 | node.next.prev = node.prev; 27 | this.tail.prev.next = node; 28 | node.prev = this.tail.prev; 29 | node.next = this.tail; 30 | this.tail.prev = node; 31 | return node.value; 32 | } 33 | 34 | lib.LRUCache.prototype.put = function(key, value, ttl) 35 | { 36 | if (this.get(key) !== undefined) { 37 | this.tail.prev.value = value; 38 | return true; 39 | } 40 | if (this.map.size === this.max) { 41 | this.map.delete(this.head.next.key); 42 | this.head.next = this.head.next.next; 43 | this.head.next.prev = this.head; 44 | } 45 | const node = { value, ttl }; 46 | this.map.set(key, node); 47 | this.tail.prev.next = node; 48 | node.prev = this.tail.prev; 49 | node.next = this.tail; 50 | this.tail.prev = node; 51 | } 52 | 53 | lib.LRUCache.prototype.del = function(key) 54 | { 55 | const node = this.map.get(key); 56 | if (node == undefined) return false; 57 | node.prev.next = node.next; 58 | node.next.prev = node.prev; 59 | if (node == this.head) this.head = node.next; 60 | if (node == this.tail) this.tail = node.prev; 61 | this.map.delete(key); 62 | return true; 63 | } 64 | 65 | lib.LRUCache.prototype.clean = function() 66 | { 67 | const now = Date.now(), s = this.map.size; 68 | for (const [key, val] of this.map) { 69 | if (val.ttl && val.ttl < now) this.del(key); 70 | } 71 | return s - this.map.size; 72 | } 73 | 74 | -------------------------------------------------------------------------------- /lib/lib/uuid.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const crypto = require('crypto'); 7 | const lib = require(__dirname + '/../lib'); 8 | const os = require('os'); 9 | 10 | // Return unique Id (UUID v4) without any special characters and in lower case 11 | lib.uuid = function(prefix) 12 | { 13 | return (prefix || "") + crypto.randomUUID().replace(/[-]/g, '').toLowerCase(); 14 | } 15 | 16 | // Generate a 22 chars slug from an UUID, alphabet can be provided, default is `lib.uriSafe` 17 | lib.slug = function(options) 18 | { 19 | var bits = "0000" + BigInt("0x" + lib.uuid()).toString(2); 20 | var bytes = []; 21 | for (let i = 0; i < bits.length; i += 6) bytes.push(bits.substr(i, 6)); 22 | const alphabet = options?.alphabet || lib.uriSafe; 23 | return (options?.prefix || "") + bytes.map((x) => alphabet[parseInt(x, 2) % alphabet.length]).join(""); 24 | } 25 | 26 | // Returns a short unique id within a microsecond 27 | lib.suuid = function(prefix, options) 28 | { 29 | var hashid = this.getHashid(options); 30 | var tm = options?.epoch ? lib.localEpoch("tm") : lib.getTimeOfDay(); 31 | var s = hashid.encode(tm[0], tm[1], hashid._counter); 32 | return prefix ? prefix + s : s; 33 | } 34 | 35 | // Generate a SnowFlake unique id as 64-bit number 36 | // Format: time - 41 bit, node - 10 bit, counter - 12 bit 37 | // Properties can be provided: 38 | // - now - time, if not given local epoch clock is used in microseconds 39 | // - epoch - local epoch type, default is milliseconds, `m` for microseconds, `s` for seconds 40 | // - node - node id, limited to max 1024 41 | // - radix - default is 10, use any value between 2 - 36 for other numeric encoding 42 | lib.sfuuid = function(options) 43 | { 44 | var node = options?.node || lib.sfuuidNode; 45 | if (node === undefined) { 46 | var intf = lib.networkInterfaces()[0]; 47 | if (intf) lib.sfuuidNode = node = lib.murmurHash3(intf.mac); 48 | } 49 | var now = options?.now || lib.localEpoch(options?.epoch); 50 | var n = BigInt(now) << 22n | (BigInt(node % 1024) << 12n) | BigInt(lib.sfuuidCounter++ % 4096); 51 | return n.toString(options?.radix || 10); 52 | } 53 | 54 | lib.sfuuidCounter = 0; 55 | 56 | // Parse an id into original components: now, node, counter 57 | lib.sfuuidParse = function(id) 58 | { 59 | const _map = { now: [22n, 64n], node: [12n, 10n], counter: [0n, 12n] }; 60 | const rc = {}; 61 | try { 62 | id = rc.id = BigInt(id); 63 | for (const p in _map) { 64 | rc[p] = Number((id & (((1n << _map[p][1]) - 1n) << _map[p][0])) >> _map[p][0]); 65 | } 66 | } catch (e) {} 67 | return rc; 68 | } 69 | 70 | // Returns time sortable unique id, inspired by https://github.com/paixaop/node-time-uuid 71 | lib.tuuid = function(prefix, encode) 72 | { 73 | if (!this._hostHash) { 74 | var b = Buffer.from(crypto.createHash('sha512').update(os.hostname(), 'ascii').digest('binary')); 75 | this._hostHash = Buffer.from([b[1], b[3], b[5], (process.pid) & 0xFF, (process.pid >> 8) & 0xFF ]); 76 | this._hostCounter = 0; 77 | } 78 | // Must fit into 3 bytes only 79 | if (++this._hostCounter >= 8388607) this._hostCounter = 1; 80 | var tm = this.getTimeOfDay(); 81 | var s = Buffer.from([tm[0] >> 24, 82 | tm[0] >> 16, 83 | tm[0] >> 8, 84 | tm[0], 85 | tm[1] >> 16, 86 | tm[1] >> 8, 87 | tm[1], 88 | this._hostHash[0], 89 | this._hostHash[1], 90 | this._hostHash[2], 91 | this._hostHash[3], 92 | this._hostHash[4], 93 | this._hostCounter >> 16, 94 | this._hostCounter >> 8, 95 | this._hostCounter ]); 96 | if (encode != "binary") s = s.toString(encode || "hex"); 97 | return prefix ? prefix + s : s; 98 | } 99 | 100 | // Return time in milliseconds from the time uuid 101 | lib.tuuidTime = function(str) 102 | { 103 | if (typeof str != "string" || !str) return 0; 104 | var idx = str.indexOf("_"); 105 | if (idx > 0) str = str.substr(idx + 1); 106 | var bytes = Buffer.from(str, 'hex'); 107 | var secs = bytes.length > 4 ? bytes.readUInt32BE(0) : 0; 108 | var usecs = bytes.length > 7 ? bytes.readUInt32BE(3) & 0x00FFFFFF : 0; 109 | return secs*1000 + (usecs/1000); 110 | } 111 | 112 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | // Based on https://github.com/felixge/node-measured 6 | // 7 | 8 | exports.name = "metrics"; 9 | exports.Counter = require('./metrics/Counter'); 10 | exports.Histogram = require('./metrics/Histogram'); 11 | exports.Meter = require('./metrics/Meter'); 12 | exports.Timer = require('./metrics/Timer'); 13 | exports.TokenBucket = require('./metrics/TokenBucket'); 14 | exports.Trace = require('./metrics/Trace'); 15 | exports.FakeTrace = require('./metrics/FakeTrace'); 16 | 17 | // Convert all metrics for all propeties. 18 | // Options: 19 | // - reset - true to reset all metrics 20 | // - take - regexp for variable that should use `take` i.e. resetable counters 21 | // - skip - regexp of properties to ignore 22 | exports.toJSON = function(obj, options) 23 | { 24 | var rc = {}; 25 | for (const p in obj) { 26 | const type = typeof obj[p]; 27 | if (!obj[p] || type == "function") continue; 28 | if (options?.skip?.test && options.skip.test(p)) continue; 29 | 30 | if (typeof obj[p].toJSON == "function") { 31 | rc[p] = obj[p].toJSON(options); 32 | } else 33 | if (type == "object") { 34 | rc[p] = this.toJSON(obj[p], options); 35 | } else 36 | if (type == "number") { 37 | rc[p] = obj[p]; 38 | if (options?.take?.test && options.take.test(p)) { 39 | obj[p] = 0; 40 | } 41 | } 42 | } 43 | return rc; 44 | } 45 | 46 | // Increments a counter in an object, creates a new var if not exist or not a number 47 | exports.incr = function(obj, name, count) 48 | { 49 | if (typeof obj[name] != "number") obj[name] = 0; 50 | obj[name] += typeof count == "number" ? count : 1; 51 | return obj[name]; 52 | } 53 | 54 | // Return the value for the given var and resets it to 0 55 | exports.take = function(obj, name) 56 | { 57 | if (typeof obj[name] !== "number") return 0; 58 | const n = obj[name]; 59 | obj[name] = 0; 60 | return n; 61 | } 62 | 63 | -------------------------------------------------------------------------------- /lib/metrics/Counter.js: -------------------------------------------------------------------------------- 1 | // 2 | // Counter that resets itself after each read 3 | // 4 | 5 | class Counter { 6 | 7 | constructor(options) { 8 | this.count = 0; 9 | } 10 | 11 | toJSON() { 12 | const n = this.count; 13 | this.count = 0; 14 | return n; 15 | } 16 | 17 | incr(count) { 18 | this.count += typeof count == "number" ? count : 1; 19 | return this.count; 20 | } 21 | } 22 | 23 | module.exports = Counter; 24 | -------------------------------------------------------------------------------- /lib/metrics/ExponentiallyMovingWeightedAverage.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = ExponentiallyMovingWeightedAverage; 3 | 4 | function ExponentiallyMovingWeightedAverage(rateUnit, tickInterval) 5 | { 6 | this._rateUnit = rateUnit || 60000; 7 | this._tickInterval = tickInterval || 5000; 8 | this._alpha = 1 - Math.exp(-this._tickInterval / this._rateUnit); 9 | this._count = 0; 10 | this._rate = 0; 11 | } 12 | 13 | ExponentiallyMovingWeightedAverage.prototype.update = function(n) 14 | { 15 | this._count += n; 16 | } 17 | 18 | ExponentiallyMovingWeightedAverage.prototype.tick = function() 19 | { 20 | this._rate += this._alpha * ((this._count / this._tickInterval) - this._rate); 21 | this._count = 0; 22 | } 23 | 24 | ExponentiallyMovingWeightedAverage.prototype.rate = function(timeUnit) 25 | { 26 | return this._rate * timeUnit || 0; 27 | } 28 | -------------------------------------------------------------------------------- /lib/metrics/FakeTrace.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | module.exports = FakeTrace; 4 | 5 | function FakeTrace() 6 | { 7 | this.start = () => (new FakeTrace()); 8 | this.stop = lib.noop; 9 | this.send = lib.noop; 10 | this.toString = () => (""); 11 | this.destroy = lib.noop; 12 | } 13 | -------------------------------------------------------------------------------- /lib/metrics/Histogram.js: -------------------------------------------------------------------------------- 1 | // 2 | // - Histogram - Keeps a resevoir of statistically relevant values biased towards the last 5 minutes to explore their distribution 3 | // - count: The number of observed values. 4 | // - min: The lowest observed value. 5 | // - max: The highest observed value. 6 | // - mean: The average of all observed values. 7 | // - dev: The standard deviation of all observed values. 8 | // - med: median, 50% of all values in the resevoir are at or below this value. 9 | // - p75: See median, 75% percentile. 10 | // - p95: See median, 95% percentile. 11 | // - p99: See median, 99% percentile. 12 | // - p999: See median, 99.9% percentile. 13 | 14 | const perf_hooks = require("perf_hooks"); 15 | 16 | class Histogram { 17 | 18 | constructor(options) { 19 | if (options?.reset) this._reset = options?.reset; 20 | this._handle = perf_hooks.createHistogram(); 21 | } 22 | 23 | update(value) { 24 | this.lastUpdate = Date.now(); 25 | this._handle.record(typeof value == "number" && value > 0 ? value : 1); 26 | } 27 | 28 | reset() { 29 | this._handle.reset(); 30 | } 31 | 32 | toJSON(options) { 33 | this.lastJSON = Date.now(); 34 | 35 | const rc = { 36 | count: this._handle.count, 37 | min: this._handle.min, 38 | max: this._handle.max, 39 | mean: this._handle.mean, 40 | dev: this._handle.stddev, 41 | med: this._handle.percentile(50), 42 | p25: this._handle.percentile(25), 43 | p75: this._handle.percentile(75), 44 | p95: this._handle.percentile(95), 45 | p99: this._handle.percentile(99), 46 | }; 47 | if (this._reset || options?.reset) this.reset(); 48 | return rc; 49 | } 50 | } 51 | 52 | module.exports = Histogram; 53 | 54 | -------------------------------------------------------------------------------- /lib/metrics/Meter.js: -------------------------------------------------------------------------------- 1 | // 2 | // - Meter - Things that are measured as events / interval. 3 | // - count: The total of all values added to the meter. 4 | // - rate: The rate of the meter since the last toJSON() call. 5 | // - mean: The average rate since the meter was started. 6 | // - m1: The rate of the meter biased towards the last 1 minute. 7 | // - m5: The rate of the meter biased towards the last 5 minutes. 8 | // - m15: The rate of the meter biased towards the last 15 minutes. 9 | 10 | const ExponentiallyMovingWeightedAverage = require('./ExponentiallyMovingWeightedAverage'); 11 | 12 | class Meter { 13 | 14 | constructor(options) { 15 | if (options?.reset) this._reset = options?.reset; 16 | this._unit = options?.unit || 1000; 17 | this._interval = options?.interval || 5000; 18 | this._init(); 19 | } 20 | 21 | _init() { 22 | this._m1Rate = new ExponentiallyMovingWeightedAverage(60000, this._interval); 23 | this._m5Rate = new ExponentiallyMovingWeightedAverage(5 * 60000, this._interval); 24 | this._m15Rate = new ExponentiallyMovingWeightedAverage(15 * 60000, this._interval); 25 | this._count = this._sum = 0; 26 | } 27 | 28 | mark(value) { 29 | if (!this._timer) this.start(); 30 | value = typeof value == "number" && value || 1; 31 | this._count += value; 32 | this._sum += value; 33 | this._m1Rate.update(value); 34 | this._m5Rate.update(value); 35 | this._m15Rate.update(value); 36 | this.lastMark = Date.now(); 37 | } 38 | 39 | start() { 40 | clearInterval(this._timer); 41 | this._timer = setInterval(this._tick.bind(this), this._interval); 42 | this.startTime = this.lastJSON = this.lastMark = Date.now(); 43 | } 44 | 45 | end() { 46 | clearInterval(this._timer); 47 | delete this._timer; 48 | } 49 | 50 | _tick() { 51 | this._m1Rate.tick(); 52 | this._m5Rate.tick(); 53 | this._m15Rate.tick(); 54 | } 55 | 56 | reset() { 57 | this.end(); 58 | this._init(); 59 | } 60 | 61 | meanRate() { 62 | if (this._count === 0) return 0; 63 | return this._count / (Date.now() - this.startTime) * this._unit; 64 | } 65 | 66 | currentRate() { 67 | var now = Date.now(); 68 | var duration = now - this.lastJSON; 69 | var rate = duration ? this._sum / duration * this._unit : 0; 70 | this._sum = 0; 71 | this.lastJSON = now; 72 | return rate; 73 | } 74 | 75 | toJSON(options) { 76 | const rc = { 77 | count: this._count, 78 | rate: this.currentRate(), 79 | mean: this.meanRate(), 80 | m1: this._m1Rate.rate(this._unit), 81 | m5: this._m5Rate.rate(this._unit), 82 | m15: this._m15Rate.rate(this._unit), 83 | }; 84 | if (this._reset || options?.reset) this.reset(); 85 | return rc; 86 | } 87 | 88 | } 89 | 90 | module.exports = Meter; 91 | 92 | -------------------------------------------------------------------------------- /lib/metrics/Timer.js: -------------------------------------------------------------------------------- 1 | // 2 | // Timers are a combination of Meters and Histograms. 3 | // They measure the rate as well as distribution of scalar events. 4 | 5 | 6 | const Meter = require("./Meter"); 7 | const Histogram = require("./Histogram"); 8 | 9 | function noop() {} 10 | 11 | class Timer { 12 | 13 | constructor(options) { 14 | this.meter = new Meter(options); 15 | this.histogram = new Histogram(options); 16 | } 17 | 18 | start(value) { 19 | var timer = { start: Date.now(), count: value }; 20 | timer.end = this._end.bind(this, timer); 21 | return timer; 22 | } 23 | 24 | _end(timer) { 25 | timer.elapsed = Date.now() - timer.start; 26 | this.update(timer.elapsed, timer.count); 27 | timer.end = noop; 28 | return timer.elapsed; 29 | } 30 | 31 | update(time, count) { 32 | this.lastUpdate = Date.now(); 33 | this.histogram.update(time); 34 | this.meter.mark(count); 35 | } 36 | 37 | reset() { 38 | this.meter.reset(); 39 | this.histogram.reset(); 40 | } 41 | 42 | end() { 43 | this.meter.end(); 44 | } 45 | 46 | toJSON(options) { 47 | return { 48 | meter: this.meter.toJSON(options), 49 | histogram: this.histogram.toJSON(options), 50 | } 51 | } 52 | } 53 | 54 | module.exports = Timer; 55 | 56 | -------------------------------------------------------------------------------- /lib/metrics/TokenBucket.js: -------------------------------------------------------------------------------- 1 | // Create a Token Bucket object for rate limiting as per http://en.wikipedia.org/wiki/Token_bucket 2 | // - rate - the rate to refill tokens 3 | // - max - the maximum burst capacity 4 | // - interval - interval for the bucket refills, default 1000 ms 5 | // 6 | // Store as an array for easier serialization into JSON when keep it in the shared cache. 7 | // 8 | // Based on https://github.com/thisandagain/micron-throttle 9 | // 10 | 11 | const lib = require("../lib"); 12 | 13 | module.exports = TokenBucket; 14 | 15 | function TokenBucket(rate, max, interval) 16 | { 17 | this.configure(rate, max, interval); 18 | } 19 | 20 | // Initialize existing token with numbers for rate calculations 21 | TokenBucket.prototype.configure = function(rate, max, interval, total) 22 | { 23 | if (Array.isArray(rate)) { 24 | this._rate = lib.toNumber(rate[0]); 25 | this._max = lib.toNumber(rate[1]); 26 | this._count = lib.toNumber(rate[2]); 27 | this._time = lib.toNumber(rate[3]); 28 | this._interval = lib.toNumber(rate[4]); 29 | this._total = lib.toNumber(rate[5]); 30 | } else 31 | if (typeof rate == "object" && rate.rate) { 32 | this._rate = lib.toNumber(rate.rate); 33 | this._max = lib.toNumber(rate.max); 34 | this._count = lib.toNumber(rate.count); 35 | this._time = lib.toNumber(rate.time); 36 | this._interval = lib.toNumber(rate.interval); 37 | this._total = lib.toNumber(rate.total); 38 | } else { 39 | this._rate = lib.toNumber(rate, { min: 0 }); 40 | this._max = lib.toNumber(max, { min: 0 }) || this._rate; 41 | this._count = this._max; 42 | this._time = Date.now(); 43 | this._interval = lib.toNumber(interval, { min: 0 }) || 1000; 44 | this._total = lib.toNumber(total, { min: 0 }); 45 | } 46 | } 47 | 48 | // Return a JSON object to be serialized/saved 49 | TokenBucket.prototype.toJSON = function() 50 | { 51 | return { rate: this._rate, max: this._max, count: this._count, time: this._time, interval: this._interval, total: this._total }; 52 | } 53 | 54 | // Return a string to be serialized/saved 55 | TokenBucket.prototype.toString = function() 56 | { 57 | return this.toArray().join(","); 58 | } 59 | 60 | // Return an array object to be serialized/saved 61 | TokenBucket.prototype.toArray = function() 62 | { 63 | return [this._rate, this._max, this._count, this._time, this._interval, this._total]; 64 | } 65 | 66 | // Return true if this bucket uses the same rates in arguments 67 | TokenBucket.prototype.equal = function(rate, max, interval) 68 | { 69 | rate = lib.toNumber(rate, { min: 0 }); 70 | max = lib.toNumber(max || rate, { min: 0 }); 71 | interval = lib.toNumber(interval || 1000, { min: 1 }); 72 | return this._rate === rate && this._max === max && this._interval == interval; 73 | } 74 | 75 | // Consume N tokens from the bucket, if no capacity, the tokens are not pulled from the bucket. 76 | // 77 | // Refill the bucket by tracking elapsed time from the last time we touched it. 78 | // 79 | // min(totalTokens, current + (fillRate * elapsedTime)) 80 | // 81 | TokenBucket.prototype.consume = function(tokens) 82 | { 83 | var now = Date.now(); 84 | if (now < this._time) this._time = now - this._interval; 85 | this._elapsed = now - this._time; 86 | if (this._count < this._max) this._count = Math.min(this._max, this._count + this._rate * (this._elapsed / this._interval)); 87 | this._time = now; 88 | if (typeof tokens != "number" || tokens < 0) tokens = 0; 89 | this._total += tokens; 90 | if (tokens > this._count) return false; 91 | this._count -= tokens; 92 | return true; 93 | } 94 | 95 | // Returns number of milliseconds to wait till number of tokens can be available again 96 | TokenBucket.prototype.delay = function(tokens) 97 | { 98 | return Math.max(0, this._interval - (tokens >= this._max ? 0 : this._elapsed)); 99 | } 100 | -------------------------------------------------------------------------------- /lib/metrics/Trace.js: -------------------------------------------------------------------------------- 1 | // AWS X-Ray trace support 2 | // 3 | // Only supports local daemon UDP port 2000, to test locally 4 | // 5 | // socat -U -v PIPE udp-recv:2000 6 | // 7 | // Example: 8 | // 9 | // var trace = new metrics.Trace({ _host: "127.0.0.1", annotations: { tag: core.onstance.tag, role: core.role } }); 10 | // var sub1 = trace.start("subsegment1"); 11 | // sub1.stop(); 12 | // var sub2 = trace.start("subsegment2"); 13 | // trace.stop(req); 14 | // trace.send(); 15 | // trace.destroy(); 16 | // 17 | 18 | const lib = require("../lib"); 19 | const logger = require("../logger"); 20 | 21 | module.exports = Trace; 22 | function Trace(options, parent) 23 | { 24 | if (parent instanceof Trace) { 25 | this._parent = parent; 26 | } else { 27 | this.trace_id = `1-${Math.round(new Date().getTime() / 1000).toString(16)}-${lib.randomBytes(12)}`; 28 | } 29 | this.id = lib.randomBytes(8); 30 | 31 | this._start = Date.now(); 32 | this.start_time = this._start / 1000; 33 | 34 | if (typeof options == "string") { 35 | this.name = options; 36 | } else { 37 | for (const p in options) { 38 | if (this[p] === undefined) this[p] = options[p]; 39 | } 40 | } 41 | if (!this.name) this.name = process.title.split(/[^a-z0-9_-]/i)[0]; 42 | } 43 | 44 | // Closes a segment or subsegment, for segments it sends it right away 45 | Trace.prototype.stop = function(req) 46 | { 47 | if (!this.end_time) { 48 | this._end = Date.now(); 49 | this.end_time = this._end / 1000; 50 | } 51 | 52 | if (req?.res?.statusCode) { 53 | this.http = { 54 | request: { 55 | method: req.method || "GET", 56 | url: `http${req.options.secure}://${req.options.host}${req.url}`, 57 | }, 58 | response: { 59 | status: req.res.statusCode 60 | } 61 | } 62 | } 63 | for (const i in this.subsegments) this.subsegments[i].stop(); 64 | } 65 | 66 | Trace.prototype.destroy = function() 67 | { 68 | for (const i in this.subsegments) this.subsegments[i].destroy(); 69 | for (const p in this) if (typeof this[p] == "object") delete this[p]; 70 | } 71 | 72 | Trace.prototype.toString = function(msg) 73 | { 74 | return lib.stringify(msg || this, (key, val) => (key[0] == "_" ? undefined : val)) 75 | } 76 | 77 | var _sock; 78 | 79 | // Sends a segment to local daemon 80 | Trace.prototype.send = function(msg) 81 | { 82 | if (!_sock) { 83 | _sock = require("node:dgram").createSocket('udp4').unref(); 84 | } 85 | 86 | var json = this.toString(msg); 87 | 88 | _sock.send(`{"format":"json","version":1}\n${json}`, this._port || 2000, this._host, (err) => { 89 | logger.logger(err ? "error": "debug", "trace", "send:", err, json); 90 | }); 91 | } 92 | 93 | // Starts a new subsegment 94 | Trace.prototype.start = function(options) 95 | { 96 | var sub = new Trace(options, this); 97 | if (!this.subsegments) this.subsegments = []; 98 | this.subsegments.push(sub); 99 | return sub; 100 | } 101 | 102 | -------------------------------------------------------------------------------- /lib/push/webpush.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const logger = require(__dirname + '/../logger'); 7 | const lib = require(__dirname + '/../lib'); 8 | const api = require(__dirname + '/../api'); 9 | 10 | const mod = { 11 | name: "webpush", 12 | 13 | create: function(options) { 14 | if (["webpush", "wp"].includes(options?.type)) return new WebpushClient(options); 15 | }, 16 | 17 | configure: function(options) { 18 | api.registerAccessCheck('', '/js/webpush.js', function(req, cb) { 19 | req.res.header("Service-Worker-Allowed", "/"); 20 | cb(); 21 | }); 22 | }, 23 | 24 | properties: ["actions", "badge", "body", "dir", "icon", "image", "lang", "renotify", "requireInteraction", "silent", "tag", "timestamp", "vibrate"], 25 | }; 26 | 27 | module.exports = mod; 28 | 29 | 30 | // Send a Web push notification using the `web-push` npm module, referer to it for details how to generate VAPID credentials to 31 | // configure this module with 3 required parameters: 32 | // 33 | // - `key` - VAPID private key 34 | // - `pubkey` - VAPID public key 35 | // - `email` - an admin email for the VAPID subject 36 | // 37 | // The device token must be generated in the browser after successful subscription: 38 | // 39 | // navigator.serviceWorker.register("/js/webpush.js", { scope: "/" }).then(function(registration) { 40 | // registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidKeyPublic }).then(function(subscription) { 41 | // bkjs.send({ url: '/user/update', data: { device_id: "wp://" + window.btoa(JSON.stringify(subscription)) }, type: "POST" }); 42 | // }).catch((err) => {}) 43 | // }); 44 | // 45 | 46 | class WebpushClient { 47 | 48 | constructor(options) { 49 | this.type = options.type; 50 | this.key = options.key; 51 | this.pubkey = options.pubkey; 52 | this.subject = `mailto:${options.email}`; 53 | this.app = options.app; 54 | this.queue = 0; 55 | } 56 | 57 | close() {} 58 | 59 | send(dev, options, callback) { 60 | if (!dev?.id) return lib.tryCall(callback, lib.newError("invalid device:" + dev.id)); 61 | 62 | var to = lib.jsonParse(Buffer.from(dev.id, "base64").toString()); 63 | if (!to) return lib.tryCall(callback, lib.newError("invalid device id:" + dev.id)); 64 | 65 | var msg = { title: options.title, body: options.msg, data: {} }; 66 | for (const p of mod.properties) { 67 | if (typeof options[p] != "undefined") msg[p] = options[p]; 68 | } 69 | 70 | if (options.id) msg.data.id = String(options.id); 71 | if (options.url) msg.data.url = String(options.url); 72 | if (options.type) msg.data.type = String(options.type); 73 | if (options.user_id) msg.data.user_id = options.user_id; 74 | for (const p in options.payload) msg.data[p] = options.payload[p]; 75 | 76 | const opts = { 77 | vapidDetails: { 78 | subject: this.subject, 79 | publicKey: this.pubkey, 80 | privateKey: this.key, 81 | } 82 | } 83 | this.queue++; 84 | this.webpush.sendNotification(to, lib.stringify(msg), opts). 85 | then(() => { 86 | this.queue--; 87 | logger.debug("send:", mod.name, dev, msg); 88 | lib.tryCall(callback); 89 | }). 90 | catch((err) => { 91 | this.queue--; 92 | logger.error("send:", mod.name, err, dev, msg); 93 | lib.tryCall(callback, err); 94 | }); 95 | return true; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/queue/local.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2024 4 | // 5 | 6 | const core = require("../core"); 7 | const lib = require(__dirname + "/../lib"); 8 | const Client = require(__dirname + "/client"); 9 | 10 | // Client that uses the local process or master process for jobs. 11 | 12 | const client = { 13 | name: "local", 14 | 15 | create: function(options) { 16 | if (/^local:/.test(options?.url)) return new LocalClient(options); 17 | } 18 | }; 19 | module.exports = client; 20 | 21 | class LocalClient extends Client { 22 | 23 | constructor(options) { 24 | super(options); 25 | this.name = client.name; 26 | 27 | this.applyOptions(); 28 | this.emit("ready"); 29 | } 30 | 31 | listen(options, callback) {} 32 | 33 | submit(msg, options, callback) { 34 | msg = lib.jsonParse(msg); 35 | setTimeout(core.modules.jobs.processJobMessage.bind(core.modules.jobs, "#local", msg), options?.delay); 36 | lib.tryCall(callback); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /lib/queue/worker.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2024 4 | // 5 | 6 | const cluster = require("cluster"); 7 | const lib = require(__dirname + "/../lib"); 8 | const Client = require(__dirname + "/client"); 9 | 10 | // Client that uses master process rate limiter and workers for jobs. 11 | 12 | const client = { 13 | name: "worker", 14 | 15 | create: function(options) { 16 | if (/^worker:/.test(options?.url)) return new WorkerClient(options); 17 | } 18 | }; 19 | module.exports = client; 20 | 21 | class WorkerClient extends Client { 22 | 23 | constructor(options) { 24 | super(options); 25 | this.name = client.name; 26 | this.qworker = 0; 27 | this.applyOptions(); 28 | this.emit("ready"); 29 | } 30 | 31 | listen(options, callback) {} 32 | 33 | submit(msg, options, callback) { 34 | var err; 35 | 36 | msg = lib.jsonParse(msg); 37 | 38 | if (cluster.isMaster) { 39 | var workers = lib.getWorkers({ worker_type: null }) 40 | if (workers.length) { 41 | msg.__op = "worker:job"; 42 | try { 43 | workers[this.qworker++ % workers.length].send(msg) 44 | } catch (e) { err = e } 45 | } else { 46 | err = { status: 404, message: "no workers available" } 47 | } 48 | } else { 49 | err = { status: 400, message: "not a master" }; 50 | } 51 | if (typeof callback == "function") callback(err); 52 | } 53 | 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2024 4 | // 5 | 6 | require('backendjs').server.start(); 7 | 8 | -------------------------------------------------------------------------------- /lib/shell.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const core = require(__dirname + '/core'); 7 | const lib = require(__dirname + '/lib'); 8 | const logger = require(__dirname + '/logger'); 9 | const ipc = require(__dirname + '/ipc'); 10 | 11 | // Shell command interface for `bksh` 12 | // 13 | // This module is supposed to be extended with commands, the format is `shell.cmdNAME`` 14 | // 15 | // where `NAME` is he commnd name in camel case 16 | // 17 | // For example: 18 | // 19 | // ```javascript 20 | //const bkjs = require("backendjs"); 21 | //const shell = bkjs.shell; 22 | // 23 | //shell.cmdMyCommand = function(options) { console.log("hello"); return "continue" } 24 | //``` 25 | // Now if i call `bksh -my-command` it will print hello and launch the repl, 26 | // instead of retuning continue if the command must exit jut call `process.exit()` 27 | // 28 | // Run `bksh -shell-help` to see all registered shell commands 29 | // 30 | 31 | // Start REPL shell or execute any subcommand if specified in the command line. 32 | // A subcommand may return special string to indicate how to treat the flow: 33 | // - stop - stop processing commands and create REPL 34 | // - continue - do not exit and continue processing other commands or end with REPL 35 | // - all other values will result in returning from the run assuming the command will decide what to do, exit or continue running, no REPL is created 36 | // - `-noexit` - in the command line keep the shell running after executing the command 37 | // - `-exit` - exit with error if no shell command found 38 | // - `-exit-timeout MS` - will be set to ms to wait before exit 39 | // - `-shell-delay MS` - will wait before running the command 40 | module.exports = function(options) 41 | { 42 | const shell = core.modules.shell; 43 | require(__dirname + "/shell/aws"); 44 | require(__dirname + "/shell/db"); 45 | require(__dirname + "/shell/shell"); 46 | require(__dirname + "/shell/user"); 47 | require(__dirname + "/shell/test"); 48 | 49 | shell.exitTimeout = lib.getArgInt("-exit-timeout", 1000); 50 | var delay = lib.getArgInt("-shell-delay"); 51 | 52 | core.runMethods("configureShell", options, (err) => { 53 | if (options.done) process.exit(); 54 | 55 | if (core.isMaster) { 56 | ipc.initServer(); 57 | } else { 58 | ipc.initWorker(); 59 | } 60 | 61 | for (var i = 1; i < process.argv.length; i++) { 62 | if (process.argv[i][0] != '-') continue; 63 | var name = lib.toCamel("cmd" + process.argv[i]); 64 | if (typeof shell[name] != "function") continue; 65 | shell.cmdName = name; 66 | shell.cmdIndex = i; 67 | if (delay) { 68 | return setTimeout(shell[name].bind(shell, options), delay); 69 | } 70 | var rc = shell[name](options); 71 | logger.debug("start:", shell.name, name, rc); 72 | 73 | if (rc == "stop") break; 74 | if (rc == "continue") continue; 75 | if (lib.isArg("-noexit")) continue; 76 | return; 77 | } 78 | if (!shell.cmdName && lib.isArg("-exit")) { 79 | return shell.exit("no shell command found"); 80 | } 81 | if (core.isMaster) { 82 | core.modules.repl = core.createRepl({ file: core.repl.file, size: core.repl.size }); 83 | core.modules.repl.on('exit', () => { 84 | core.runMethods("shutdownShell", { sync: 1 }, () => { 85 | setTimeout(() => { process.exit() }, shell.exitTimeout); 86 | }); 87 | }); 88 | } 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /lib/shell/user.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const core = require(__dirname + '/../core'); 7 | const users = require(__dirname + '/../users'); 8 | const lib = require(__dirname + '/../lib'); 9 | const shell = core.modules.shell; 10 | const apiusers = core.modules.api.users; 11 | 12 | shell.help.push( 13 | "-user-add login LOGIN secret SECRET [name NAME] [type TYPE] ... - add a new user record for API access", 14 | "-user-update login LOGIN [name NAME] [type TYPE] ... - update existing user record", 15 | "-user-del ID|LOGIN... - delete a user record", 16 | "-user-get ID|LOGIN ... - show user records", 17 | "-api-user-add login LOGIN secret SECRET [name NAME] [type TYPE] ... - add a new API user", 18 | "-api-user-update login LOGIN [name NAME] [type TYPE] ... - update existing API user", 19 | "-api-user-del ID|LOGIN... - delete an API user", 20 | ); 21 | 22 | 23 | // Show user records by id or login 24 | shell.cmdUserGet = function(options) 25 | { 26 | lib.forEachSeries(process.argv.slice(2).filter((x) => (x[0] != "-")), function(login, next) { 27 | users.get(login, (err, row) => { 28 | if (row) shell.jsonFormat(row); 29 | next(); 30 | }); 31 | }, this.exit); 32 | } 33 | 34 | // Add a user login 35 | shell.cmdUserAdd = function(options) 36 | { 37 | users.add(this.getQuery(), lib.objExtend(this.getArgs(), { isInternal: 1 }), this.exit); 38 | } 39 | 40 | // Update a user login 41 | shell.cmdUserUpdate = function(options) 42 | { 43 | users.update(this.getQuery(), lib.objExtend(this.getArgs(), { isInternal: 1 }), this.exit); 44 | } 45 | 46 | // Delete a user login 47 | shell.cmdUserDel = function(options) 48 | { 49 | lib.forEachSeries(process.argv.slice(2).filter((x) => (x[0] != "-")), (login, next) => { 50 | users.del(login, next); 51 | }, this.exit); 52 | } 53 | 54 | shell.cmdApiUserdd = function(options) 55 | { 56 | var query = this.getQuery(); 57 | var opts = lib.objExtend(this.getArgs(), { isInternal: 1 }); 58 | apiusers.add({ query, user: {}, options: opts }, opts, this.exit); 59 | } 60 | 61 | shell.cmdApiUserUpdate = function(options) 62 | { 63 | var query = this.getQuery(); 64 | var opts = lib.objExtend(this.getArgs(), { isInternal: 1 }); 65 | this.getUser(query, (user) => { 66 | apiusers.update({ user, query, options: opts }, opts, this.exit); 67 | }); 68 | } 69 | 70 | shell.cmdApiUserDel = function(options) 71 | { 72 | var query = this.getQuery(); 73 | var opts = lib.objExtend(this.getArgs(), { isInternal: 1 }); 74 | this.getUser(query, (user) => { 75 | apiusers.del({ user, query, options: opts }, this.exit); 76 | }); 77 | } 78 | 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.183.0", 3 | "type": "commonjs", 4 | "author": "Vlad Seryakov", 5 | "name": "backendjs", 6 | "description": "A platform for building backends", 7 | "main": "lib/index", 8 | "homepage": "https://github.com/vseryakov/backendjs", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vseryakov/backendjs.git" 12 | }, 13 | "dependencies": { 14 | "cookie": "1.0.2", 15 | "cookie-parser": "1.4.7", 16 | "croner": "9.0.0", 17 | "express": "4.21.2", 18 | "formidable": "3.5.4", 19 | "hashids": "2.3.0", 20 | "microtime": "3.1.1", 21 | "mime-types": "3.0.1", 22 | "nodemailer": "6.10.0", 23 | "qs": "6.14.0", 24 | "ws": "8.18.2", 25 | "xml2json": "0.12.0" 26 | }, 27 | "devDependencies": { 28 | "uglify-js": "3.19.3" 29 | }, 30 | "modDependencies": { 31 | "bcrypt": "5.1.1", 32 | "nats": "2.29.3", 33 | "pg": "8.15.6", 34 | "redis": "3.1.2", 35 | "sharp": "0.34.1", 36 | "unix-dgram": "2.0.6", 37 | "web-push": "3.6.7" 38 | }, 39 | "keywords": [ 40 | "bkjs", 41 | "webservice", 42 | "websockets", 43 | "aws", 44 | "database", 45 | "API", 46 | "DynamoDB", 47 | "Sqlite", 48 | "Elasticsearch", 49 | "PostgreSQL", 50 | "NATS", 51 | "Redis", 52 | "pubsub", 53 | "account", 54 | "messaging", 55 | "instance", 56 | "jobs", 57 | "cron" 58 | ], 59 | "engines": { 60 | "node": ">=20.0" 61 | }, 62 | "license": "BSD-3-Clause", 63 | "bin": { 64 | "bkjs": "./bkjs", 65 | "bksh": "./bkjs" 66 | }, 67 | "config": { 68 | "sync": { 69 | "path": "node_modules", 70 | "include": [ 71 | "*.gz", 72 | "*.js.map", 73 | "*.bundle.js", 74 | "*.bundle.css" 75 | ] 76 | }, 77 | "bundles": { 78 | "bkjs": { 79 | "js": [ 80 | "web/js/popper2.min.js", 81 | "web/js/bootstrap.min.js", 82 | "web/js/knockout.min.js", 83 | "web/js/bootpopup.js", 84 | "web/js/app.js", 85 | "web/js/app.ko.js", 86 | "web/js/bkjs-lib.js", 87 | "web/js/bkjs-send.js", 88 | "web/js/bkjs-conv.js", 89 | "web/js/bkjs-ws.js", 90 | "web/js/bkjs-ko.js", 91 | "web/js/bkjs-user.js", 92 | "web/js/bkjs-passkey.js", 93 | "web/js/bkjs-bootstrap.js", 94 | "web/js/alpine.js" 95 | ], 96 | "js.dev": [ 97 | "web/js/popper2.js", 98 | "web/js/bootstrap.js", 99 | "web/js/knockout.js", 100 | "web/js/bootpopup.js", 101 | "web/js/app.js", 102 | "web/js/app.ko.js", 103 | "web/js/bkjs-lib.js", 104 | "web/js/bkjs-send.js", 105 | "web/js/bkjs-conv.js", 106 | "web/js/bkjs-ws.js", 107 | "web/js/bkjs-ko.js", 108 | "web/js/bkjs-user.js", 109 | "web/js/bkjs-passkey.js", 110 | "web/js/bkjs-bootstrap.js", 111 | "web/js/alpine.js" 112 | ], 113 | "css": [ 114 | "web/css/bootstrap.css", 115 | "web/css/font-awesome.css" 116 | ] 117 | } 118 | } 119 | }, 120 | "files": [ 121 | "bkjs", 122 | "lib/", 123 | "web/", 124 | "tools/", 125 | "modules/" 126 | ], 127 | "scripts": { 128 | "start": "./bkjs run-backend", 129 | "stop": "./bkjs stop", 130 | "doc": "node tools/doc.js > web/doc.html", 131 | "build": "./bkjs bundle -all -gzip", 132 | "devbuild": "./bkjs bundle -all -dev", 133 | "test": "./bkjs test-all" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/auth.js: -------------------------------------------------------------------------------- 1 | /* global lib api core logger */ 2 | 3 | tests.test_auth = function(callback, test) 4 | { 5 | var argv = [ 6 | "-api-allow-acl-admin", "admin, manager, allow1, auth", 7 | "-api-allow-acl-manager", "manager, user", 8 | "-api-allow-acl-user", "user, allow1", 9 | "-api-allow-acl-authenticated", "auth", 10 | 11 | "-api-deny-acl-manager", "useronly", 12 | "-api-deny-acl-user", "manageronly, userdeny", 13 | "-api-deny-acl-admin", "manageronly", 14 | 15 | "-api-acl-auth", "^/auth", 16 | "-api-acl-admin", "^/admin", 17 | "-api-acl-manager", "^/manager", 18 | "-api-acl-user", "^/user", 19 | "-api-acl-allow1", "^/allow1", 20 | "-api-acl-userdeny", "^/userdeny", 21 | "-api-acl-useronly", "^/useronly", 22 | "-api-acl-manageronly", "^/manageronly", 23 | ]; 24 | 25 | var checks = [ 26 | { status: 403, path: "/system" }, 27 | { status: 403, path: "/system", type: "admin" }, 28 | 29 | { status: 200, path: "/auth" }, 30 | { status: 200, path: "/auth", type: "admin" }, 31 | { status: 200, path: "/auth", type: "user" }, 32 | 33 | { status: 403, path: "/admin" }, 34 | { status: 403, path: "/admin", type: "user" }, 35 | { status: 403, path: "/admin", type: "manager" }, 36 | { status: 200, path: "/admin", type: "admin" }, 37 | 38 | { status: 403, path: "/allow1" }, 39 | { status: 200, path: "/allow1", type: "user" }, 40 | { status: 403, path: "/allow1", type: "manager" }, 41 | { status: 200, path: "/allow1", type: "admin" }, 42 | 43 | { status: 200, path: "/user", type: "user" }, 44 | { status: 403, path: "/user", type: "admin" }, 45 | { status: 200, path: "/user", type: "manager" }, 46 | 47 | { status: 200, path: "/useronly", type: "user" }, 48 | { status: 403, path: "/useronly", type: "manager" }, 49 | 50 | { status: 200, path: "/userdeny", type: "manager" }, 51 | { status: 403, path: "/userdeny", type: "user", code: "DENY" }, 52 | { status: 403, path: "/useronly", type: "manager", code: "DENY" }, 53 | 54 | { status: 403, path: "/manager" }, 55 | { status: 403, path: "/manager", type: "user" }, 56 | { status: 200, path: "/manager", type: "manager" }, 57 | { status: 200, path: "/manager", type: "admin" }, 58 | 59 | { status: 200, path: "/manageronly", type: "manager" }, 60 | { status: 403, path: "/manageronly", type: "admin", code: "DENY" }, 61 | { status: 403, path: "/manageronly", type: "user", code: "DENY" }, 62 | 63 | ]; 64 | test.req = req; 65 | 66 | api.resetAcl(); 67 | core.parseArgs(argv); 68 | for (const p in api) { 69 | if (/^(allow|deny|acl)/.test(p) && !lib.isEmpty(api[p]) && typeof api[p] == "object") logger.info(p, "=", api[p]); 70 | } 71 | var req = { user: {}, options: {} }; 72 | 73 | lib.forEachSeries(checks, (check, next) => { 74 | req.user.id = check.type || "anon"; 75 | req.user.type = lib.strSplit(check.type); 76 | req.options.path = check.path; 77 | api.checkAuthorization(req, (err) => { 78 | if (err && err?.status != 200) logger.info(check, err); 79 | expect((err?.status || 200) === check.status, err || "no error", check); 80 | if (err && check.code !== undefined) { 81 | expect((err.code || "") === check.code, err, check); 82 | } 83 | next(); 84 | }); 85 | }, callback); 86 | } 87 | -------------------------------------------------------------------------------- /tests/config: -------------------------------------------------------------------------------- 1 | port=8999 2 | ws-port=8999 3 | api-restart=~~ 4 | no=packages 5 | 6 | db-elasticsearch-pool-tables=test1,test2,test3 7 | db-elasticsearch-pool-options-defaultParams={"refresh":true} 8 | -------------------------------------------------------------------------------- /tests/ipc.js: -------------------------------------------------------------------------------- 1 | /* global lib logger ipc cache */ 2 | 3 | tests.test_limiter = function(callback, test) 4 | { 5 | var opts = { 6 | name: lib.getArg("-name", "test"), 7 | rate: lib.getArgInt("-rate", 1), 8 | max: lib.getArgInt("-max", 1), 9 | interval: lib.getArgInt("-interval", 1000), 10 | cacheName: test.cache || lib.getArg("-test-cache", "test"), 11 | pace: lib.getArgInt("-pace", 5), 12 | count: lib.getArgInt("-count", 5), 13 | delays: lib.getArgInt("-delays", 4), 14 | }; 15 | 16 | ipc.initServer(); 17 | 18 | lib.series([ 19 | function(next) { 20 | setTimeout(next, 1000); 21 | }, 22 | function(next) { 23 | var list = [], delays = 0; 24 | for (let i = 0; i < opts.count; i++) list.push(i); 25 | lib.forEachSeries(list, function(i, next2) { 26 | lib.doWhilst( 27 | function(next3) { 28 | cache.limiter(opts, (delay) => { 29 | opts.delay = delay; 30 | logger.log("limiter:", opts); 31 | setTimeout(next3, delay); 32 | }); 33 | }, 34 | function() { 35 | if (opts.delay) delays++; 36 | return opts.delay; 37 | }, 38 | function() { 39 | setTimeout(next2, opts.pace); 40 | }); 41 | }, () => { 42 | expect(delays == opts.delays, `delays mismatch: ${delays} != ${opts.delays}`); 43 | next(); 44 | }); 45 | }, 46 | function(next) { 47 | opts.retry = 2; 48 | cache.limiter(opts, (delay, info) => { 49 | cache.checkLimiter(opts, (delay, info) => { 50 | expect(!delay && opts._retries == 2, "should wait and continue", opts, info); 51 | next(); 52 | }); 53 | }); 54 | }, 55 | function(next) { 56 | opts.retry = 1; 57 | delete opts._retries; 58 | cache.limiter(opts, (delay, info) => { 59 | cache.checkLimiter(opts, (delay, info) => { 60 | expect(delay && opts._retries == 1, "should fail after first run", opts, info); 61 | next(); 62 | }); 63 | }); 64 | }, 65 | ], callback); 66 | } 67 | 68 | -------------------------------------------------------------------------------- /tests/jobs.js: -------------------------------------------------------------------------------- 1 | /* global lib jobs core ipc sleep queue promisify */ 2 | 3 | const submitJob = promisify(jobs.submitJob.bind(jobs)); 4 | 5 | tests.test_jobs = async function(callback, test) 6 | { 7 | const queueName = test.queue || lib.getArg("-test-queue") || queue.getClient().queueName; 8 | 9 | // To avoid racing conditions and poll faster 10 | var q = queue.getClient(queueName); 11 | q.options.interval = q.options.retryInterval = 50; 12 | 13 | var file = core.path.tmp + "/" + process.pid + ".test"; 14 | var opts = { queueName }; 15 | 16 | await jobs.submitJob({ job: { "shell.testJob": { file, data: "job" } } }, opts); 17 | await sleep(1000) 18 | 19 | var data = lib.readFileSync(file); 20 | expect(/job/.test(data), "expect job finished", file, data, opts); 21 | 22 | await submitJob({ job: { "shell.testJob": { file, cancel: process.pid, timeout: 5000 } } }, opts); 23 | await sleep(1000) 24 | 25 | // jobs.cancelJob only sends to workers so we send to all shells explicitly 26 | ipc.broadcast(":" + core.role, ipc.newMsg("jobs:cancel", { key: process.pid })); 27 | await sleep(1000); 28 | 29 | data = lib.readFileSync(file); 30 | expect(/cancelled/.test(data), "expect job cancelled", file, data, opts); 31 | 32 | await submitJob({ job: { "shell.testJob": { file, data: "local" } } }, { queueName: "local" }); 33 | await sleep(1000) 34 | 35 | data = lib.readFileSync(file); 36 | expect(/local/.test(data), "expect local job", file, data); 37 | 38 | callback(); 39 | } 40 | 41 | tests.test_master_worker = async function(callback, test) 42 | { 43 | var file = core.path.tmp + "/" + process.pid + ".test"; 44 | 45 | await submitJob({ job: { "shell.testJob": { file, data: "worker" } } }, { queueName: "worker" }); 46 | await sleep(2000) 47 | 48 | var data = lib.readFileSync(file); 49 | expect(/worker/.test(data), "expect worker job", file, data); 50 | expect(!data.includes(` ${process.pid} `), "expect worker pid in worker job", file, data) 51 | 52 | callback(); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /tools/alpine/APKBUILD.cloudwatch: -------------------------------------------------------------------------------- 1 | # Maintainer: Vlad Seryakov 2 | pkgname=amazon-cloudwatch-agent 3 | pkgver=1.300054.0 4 | pkgrel=0 5 | pkgdesc="Amazon Cloudwatch Agent" 6 | url="https://github.com/aws/amazon-cloudwatch-agent" 7 | arch="all" 8 | license="MIT" 9 | makedepends=" 10 | go 11 | " 12 | source="$pkgname-$pkgver.tar.gz::https://github.com/aws/amazon-cloudwatch-agent/archive/refs/tags/v$pkgver.tar.gz" 13 | 14 | options="!check !fhs" 15 | 16 | build() { 17 | cd $srcdir/$pkgname-$pkgver 18 | go mod download -x 19 | export CWARCH=$(uname -m) 20 | [ "$CWARCH" = "x86_64" ] && export CWARCH=amd64 21 | [ "$CWARCH" = "aarch64" ] && export CWARCH=arm64 22 | echo $pkgver > CWAGENT_VERSION 23 | make build-for-docker-$CWARCH 24 | } 25 | 26 | package() { 27 | cd $srcdir/$pkgname-$pkgver 28 | CWAGENT=amazon-cloudwatch-agent 29 | destdir=$pkgdir/opt/aws/$CWAGENT 30 | mkdir -p $destdir/bin $destdir/etc/$CWAGENT.d $destdir/logs $destdir/var $destdir/doc 31 | cp build/bin/linux_$CWARCH/* $destdir/bin 32 | rm -f $destdir/bin/start-$CWAGENT 33 | cp licensing/* $destdir 34 | cp translator/config/schema.json $destdir/doc/$CWAGENT-schema.json 35 | } 36 | 37 | sha512sums=" 38 | a25a6d356607dc3a4c2f6656db97db13ee17328b869bc80164bfa24b90429a05f254bb3469e410ba79c994415f590fe72a685dc528330b9bbe5355ff49182276 amazon-cloudwatch-agent-1.300054.0.tar.gz 39 | " 40 | 41 | 42 | -------------------------------------------------------------------------------- /tools/bkjs-deps: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|deps-help) 10 | echo 11 | echo " deps [-fields dependencies,devDependencies] [-path .] [-dirs LIST] [-skip REGEXP] [-prod] [-global] [-update] [-mods] [-strict] [-check] [-npm ARGS] - install or show npm dependencies from the package.json, optional bkjs modules require -mods flag" 12 | ;; 13 | 14 | deps) 15 | NPM_BIN=$BKJS_HOME/bin/npm 16 | [ ! -f $NPM_BIN ] && NPM_BIN=$(which npm 2>/dev/null) 17 | cmd=install 18 | [ -n "$(get_flag -update)" ] && cmd=update 19 | npmargs=$(get_arg -npm) 20 | check=$(get_flag -check) 21 | strict=$(get_flag -strict) 22 | global=$(get_flag -global) 23 | skip=$(get_arg -skip) 24 | filter=$(get_arg -filter) 25 | fields=$(get_arg -fields dependencies,devDependencies) 26 | if [[ -n "$check" ]]; then 27 | strict=1 28 | fields="$fields,modDependencies" 29 | else 30 | [ -n "$(get_flag -mods)" ] && fields="$fields,modDependencies" 31 | fi 32 | path=$(get_arg -path) 33 | dirs=$(get_arg -dirs) 34 | if [ -n "$dirs" ]; then 35 | depth=$(get_arg -depth 1) 36 | path=$(find $dirs -maxdepth $depth -mindepth 1 -type d) 37 | fi 38 | for p in ${path:-.}; do 39 | [ ! -f $p/package.json ] && continue 40 | [[ "${p:0:1}" != "/" && "${p:0:1}" != "." ]] && p="./$p" 41 | m=$(node -e "try{skip='$skip';filter='$filter';p=require('$p/package.json');console.log('$fields'.split(',').map(f=>(Object.keys(p[f]||{}).filter(x=>((!filter||x.match(filter))&&!(skip&&x.match(skip)))).map(x=>(x+(!'$strict'&&p[f][x][0]=='^'?'@'+p[f][x].substr(1).split('.')[0]:'$strict'||/^[0-9]/.test(p[f][x])?'@'+p[f][x].replace(/[=<>^~]/g,''):'')).trim()).join(' '))).join(' '))}catch(e){if('$BKJS_DEBUG')console.error('$p',e)}") 42 | [ "$m" != "" ] && modules="$modules $m" 43 | done 44 | [ -z "$modules" ] && exit 0 45 | if [ -n "$check" ]; then 46 | npath=./node_modules 47 | [ -n "$global" ] && npath=$NODE_PATH 48 | mods="" 49 | for m in $modules; do 50 | node -e "m='$m',p=m.split('@').slice(0,-1).join('@');try{var v=require('$npath/'+p+'/package.json').version}catch(e){console.error(e)};l=child_process.execSync('npm v '+p+' version').toString().trim();console.log(v!=l?'!':'',m,v,l)" 51 | done 52 | exit 0 53 | fi 54 | [ -n "$(get_flag -prod)" ] && npmargs="$npmargs --omit=dev" 55 | [ -n "$global" ] && npmargs="$npmargs -g" 56 | echo "$NPM_BIN $npmargs $cmd $modules" 57 | [ -n "$(get_flag -dry-run)" ] && exit 0 58 | $NPM_BIN $npmargs $cmd $modules 59 | exit 60 | ;; 61 | 62 | esac 63 | -------------------------------------------------------------------------------- /tools/bkjs-docker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Nov 2021 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|docker-help) 10 | echo "" 11 | echo " docker-build-tag -t TAG [-version V] [-path .] [-root H] - build an image for the specified tag" 12 | echo " docker-run-tag -t TAG [-path .] [-force] - run a container for the specified tag" 13 | echo " docker-restart NAME - restart a container, for use with bkrsync" 14 | echo " docker-compose-get - install docker compose plugin in /usr/local/bin" 15 | echo " docker-ecr-login-get - install ECR login utility for docker in ~/bin" 16 | echo " docker-init-binfmt - initialize local docker to support multiple platforms in the default builder by using binfmt" 17 | ;; 18 | 19 | docker-build-tag) 20 | tag=$(get_arg -tag) 21 | version=$(get_arg -version) 22 | path=$(get_arg -path .) 23 | root=$(get_arg -root .) 24 | 25 | [ ! -f $path/Dockerfile.$tag ] && echo "$BKJS_CMD: no Dockerfile.$tag found in $path" && exit 1 26 | 27 | [ -f $path/path.$tag ] && path="$root$(head -1 $path/path.$tag)" 28 | [ ! -d $root ] && echo "$BKJS_CMD: invalid $tag context path: $root" && exit 1 29 | 30 | if [ ! -f $root/.dockerignore -a -f $path/dockerignore.$tag ]; then 31 | cp $path/dockerignore.$tag $root/.dockerignore 32 | dockerignore=yes 33 | fi 34 | 35 | [ -f $path/build.$tag ] && cmd=$(cat $path/build.$tag|tr '\n' ' ') 36 | [ ! -z $version ] && cmd="$cmd -t $tag:$version" 37 | 38 | cmd="docker build --rm --progress=plain -t $tag -f $path/Dockerfile.$tag $cmd $root" 39 | debug $cmd 40 | 41 | $cmd 42 | rc=$? 43 | 44 | [ "$dockerignore" = "yes" ] && rm -f $root/.dockerignore 45 | exit $rc 46 | ;; 47 | 48 | docker-run-tag) 49 | tag=$(get_arg -tag) 50 | path=$(get_arg -path .) 51 | 52 | [ -z $tag ] && echo "$BKJS_CMD: -tag must be provided" && exit 1 53 | [ -f $path/run.$tag ] && cmd=$(cat $path/run.$tag|tr '\n' ' ') 54 | 55 | if [ "$(get_flag -force)" != "" ]; then 56 | pids=$(docker ps -aq -f name=$tag) 57 | [ ! -z $pids ] && docker rm -f $pids 58 | fi 59 | 60 | cmd="docker run -d --name $tag ${cmd:-$tag} $(get_all_args "-tag -path -force")" 61 | debug $cmd 62 | 63 | $cmd 64 | exit 65 | ;; 66 | 67 | docker-restart) 68 | exec docker restart $BKJS_ARGV0 69 | ;; 70 | 71 | docker-init-binfmt) 72 | docker run --rm --privileged linuxkit/binfmt:312ed1cb899fae229b5303ac6c0510ac58f331c8 73 | exit 74 | ;; 75 | 76 | docker-compose-get) 77 | wget -L -O /usr/local/bin/docker-compose https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) 78 | chmod 755 /usr/local/bin/docker-compose 79 | ln -s /usr/local/bin/docker-compose /usr/libexec/docker/cli-plugins/docker-compose 80 | exit 81 | ;; 82 | 83 | docker-ecr-login-get) 84 | go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@latest 85 | mv ~/go/bin/docker-credential-ecr-login $BKJS_HOME/bin 86 | echo "{ \"auths\": {}, \"credsStore\": \"ecr-login\" }" > ~/.docker/config.json 87 | exit 88 | ;; 89 | 90 | esac 91 | -------------------------------------------------------------------------------- /tools/bkjs-dynamodb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|dynamodb-help) 10 | echo "" 11 | echo " dynamodb-init - download and install local DynamoDB, start the server" 12 | echo " dynamodb-get [-force] - install local DynamoDB server in $BKJS_HOME/dynamodb" 13 | echo " dynamodb-run [-memmax SZ] - run local DynamoDB server installed in $BKJS_HOME/dynamodb, data files in $BKJS_HOME/var" 14 | echo " dynamodb-stop - stop local DynamoDB server" 15 | echo " dynamodb-reset - remove local DynamoDB database and restart the server" 16 | ;; 17 | 18 | dynamodb-init) 19 | ($0 dynamodb-get $(get_all_args)) 20 | ($0 dynamodb-run $(get_all_args)) 21 | exit 22 | ;; 23 | 24 | dynamodb-get) 25 | [ "$DYNAMODB_PREFIX" = "" ] && DYNAMODB_PREFIX=$BKJS_HOME/dynamodb 26 | [ "$(get_flag -force)" != "" -a "$DYNAMODB_PREFIX" != "" ] && rm -rf $DYNAMODB_PREFIX 27 | if [ ! -d $DYNAMODB_PREFIX ]; then 28 | mkdir -p $DYNAMODB_PREFIX 29 | curl -L -o ddb.tgz http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz 30 | tar -C $DYNAMODB_PREFIX -xzf ddb.tgz 31 | rm -f ddb.tgz 32 | fi 33 | exit 34 | ;; 35 | 36 | dynamodb-run) 37 | [ "$DYNAMODB_PREFIX" = "" ] && DYNAMODB_PREFIX=$BKJS_HOME/dynamodb 38 | mkdir -p $BKJS_HOME/var $BKJS_HOME/log 39 | params="-Xmx$(get_arg -memmax 256M)" 40 | export DDB_LOCAL_TELEMETRY=0 41 | (cd $BKJS_HOME/var && exec nohup java $params -Djava.library.path=$DYNAMODB_PREFIX/DynamoDBLocal_lib -jar $DYNAMODB_PREFIX/DynamoDBLocal.jar -disableTelemetry -dbPath $BKJS_HOME/var -port 8181 >>$BKJS_HOME/log/ddb.log 2>&1 &) 42 | exit 43 | ;; 44 | 45 | dynamodb-stop) 46 | pkill -f DynamoDBLocal 47 | exit 48 | ;; 49 | 50 | dynamodb-reset) 51 | $0 dynamodb-stop 52 | rm -rf $BKJS_HOME/var/*_us-east-1.db 53 | $0 dynamodb-run 54 | exit 55 | ;; 56 | 57 | esac 58 | 59 | -------------------------------------------------------------------------------- /tools/bkjs-ec2-ami: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$BKJS_CMD" in 4 | 5 | help|ec2-help) 6 | echo 7 | echo " ec2-create-ami [-tag NAME] [-prefix PREFIX] - create a new AMI from the given running instance by tag or the current instance" 8 | echo " ec2-create-launch-template-version [-name NAME] [-tag TAG] [-skip A B] [-image-name *] - create new launch template verson with the latest AMI, if no -name is given all existing templates matched by tag if given will be updated" 9 | echo " ec2-build-ami - [-image-id ID] [-image-name N] [-ssh-user alpine] - build a new AMI for Alpine Linux, runs alpine-build-ami-hook for actual setup, uses -aws-launch-instance, either -image-id or -image-name is required" 10 | echo " ec2-build-ami-hook - actual new AMI build script after it is launched and ssh is ready: INSTANCE_USER, INSTANCE_HOST, INSTANCE_ID, INSTANCE_IP, INSTANCE_ARCH are set" 11 | ;; 12 | 13 | ec2-create-ami) 14 | tag=$(get_arg -tag) 15 | [ -n "$tag" ] && instance_id=$(bkjs ec2-show -tag $tag -fmt id | head -1) 16 | instance_id=$(get_arg -instance-id $instance_id) 17 | $BKJS_BIN shell -no db,ipc -aws-create-image -wait -instance-id $instance_id $(get_all_args "-instance-id -tag") 18 | exit 19 | ;; 20 | 21 | ec2-create-launch-template-version) 22 | name=$(get_arg -name) 23 | skip=$(get_arg -skip) 24 | if [ -z "$name" ]; then 25 | tag=$(get_arg -tag) 26 | key=$(get_arg -key Name) 27 | [ "$tag" != "" ] && filter="--filter Name=tag:$key,Values=${tag}" 28 | name=$(aws ec2 describe-launch-templates $filter --query 'LaunchTemplates[*].LaunchTemplateName' --output text|sed 's/\t/\n/g'|sort|uniq) 29 | fi 30 | for c in $name; do 31 | if list_has $c $skip; then continue; fi 32 | $BKJS_BIN shell -no db,ipc -aws-create-launch-template-version -name $c $(get_all_args "-name -skip -tag -key") 33 | done 34 | exit 35 | ;; 36 | 37 | ec2-build-ami) 38 | run_bkjs_cmd ec2-launch-instance 1 -return -ssh-user alpine 39 | [ "$?" != "0" ] && exit 1 40 | 41 | # Run the hooks to do the actual work now 42 | run_bkjs_cmd $BKJS_CMD-hook 1 43 | exit 44 | ;; 45 | 46 | ec2-build-ami-hook) 47 | # Default hooks to build Alpine image 48 | ssh $INSTANCE_USER@$INSTANCE_HOST "doas apk add git && git clone --depth=1 https://github.com/vseryakov/backendjs.git && doas backendjs/bkjs setup-ec2 && doas reboot" 49 | echo 50 | echo "now you can ssh into $INSTANCE_ID AS ec2-user@$INSTANCE_HOST" 51 | exit 52 | ;; 53 | 54 | esac 55 | -------------------------------------------------------------------------------- /tools/bkjs-ec2-cwagent: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | CWAGENT=amazon-cloudwatch-agent 8 | CWAROOT=/opt/aws/$CWAGENT 9 | 10 | case "$BKJS_CMD" in 11 | 12 | help|ec2-help) 13 | echo "" 14 | echo " ec2-cwagent-get - install AWS Cloudwatch agent" 15 | echo " ec2-cwagent-check-config [-root D] - check AWS Cloudwatch agent config for changes, exists with code 2 if changed" 16 | echo " ec2-cwagent-start [-root D] - start AWS Cloudwatch agent" 17 | echo " ec2-cwagent-init-monit [-root D] - setup agent to be run on start and to be monitored" 18 | ;; 19 | 20 | ec2-cwagent-get) 21 | root=$(get_arg -root $CWAROOT) 22 | if [ ! -d $root ]; then 23 | case "$OS_TYPE" in 24 | alpine) 25 | mkdir -p $root/bin $root/etc/$CWAGENT.d $root/logs $root/var 26 | (cd $root/bin && 27 | curl -OL https://amazoncloudwatch-agent.s3.amazonaws.com/nightly-build/latest/linux_$OS_ARCH/amazon-cloudwatch-agent && 28 | curl -OL https://amazoncloudwatch-agent.s3.amazonaws.com/nightly-build/latest/linux_$OS_ARCH/config-translator && 29 | chmod 755 $root/bin/*) 30 | ;; 31 | 32 | amazon) 33 | curl -OL https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/$OS_ARCH/latest/amazon-cloudwatch-agent.rpm 34 | rpm -i amazon-cloudwatch-agent.rpm 35 | rm amazon-cloudwatch-agent.rpm 36 | ;; 37 | esac 38 | fi 39 | exit 40 | ;; 41 | 42 | ec2-cwagent-start) 43 | root=$(get_arg -root $CWAROOT) 44 | [ ! -d $root ] && exit 2 45 | 46 | tag=$(get_arg -tag) 47 | [ -z "$tag" ] && tag=$($BKJS_BIN ec2-tag) 48 | 49 | config="\"agent\": { \"usage_data\": false, \"logfile\": \"$CWAROOT/logs/cwagent.log\" }" 50 | 51 | files="access.log message.log error.log docker.log $BKJS_CLOUDWATCH_LOGS" 52 | for file in $files; do 53 | [ -n "$logs" ] && logs="$logs," 54 | logs="$logs{ \"file_path\": \"$BKJS_HOME/log/$file\", \"log_group_name\": \"$file\", \"multi_line_start_pattern\": \"^[^ \\t]\", \"timestamp_format\": \"%Y-%m-%dT%H:%M:%S.%f\" }" 55 | done 56 | 57 | # Streams format: tag_group.log 58 | streams="$BKJS_CLOUDWATCH_STREAMS" 59 | for file in $streams; do 60 | [ -n "$logs" ] && logs="$logs," 61 | log=$(echo $file|awk -F_ '{print $2}') 62 | str=$(echo $file|awk -F_ '{print $1}') 63 | logs="$logs{ \"file_path\": \"$BKJS_HOME/log/$file\", \"log_group_name\": \"$log\", \"log_stream_name\": \"$str\", \"multi_line_start_pattern\": \"^[^ \\t]\", \"timestamp_format\": \"%Y-%m-%dT%H:%M:%S.%f\" }" 64 | done 65 | 66 | config="$config, \"logs\": { \"logs_collected\": { \"files\": { \"collect_list\": [ $logs ] } }, \"log_stream_name\": \"$tag\" }" 67 | 68 | config="$config, \"traces\": { \"local_mode\": true, \"traces_collected\": { \"xray\": {} } }" 69 | 70 | tmp=$root/etc/cwagent.json 71 | echo "{ $config }" > $tmp 72 | 73 | json=$root/etc/$CWAGENT.json 74 | toml=$root/etc/$CWAGENT.toml 75 | 76 | cmp $json $tmp 77 | if [ "$?" != "0" ]; then 78 | mv $tmp $json 79 | $root/bin/config-translator --input $json --output $toml --mode auto 80 | [ "$?" != "0" ] && exit 1 81 | fi 82 | 83 | toml=$root/etc/$CWAGENT.toml 84 | env=$root/etc/env-config.json 85 | echo "{ \"CWAGENT_LOG_LEVEL\": \"$(get_arg -log ERROR)\" }" > $env 86 | 87 | exec nohup $root/bin/$CWAGENT -config $toml -envconfig $env -pidfile $root/logs/cwagent.pid >> $CWAROOT/logs/cwagent.log 2>&1 & 88 | exit 0 89 | ;; 90 | 91 | ec2-cwagent-init-monit) 92 | root=$(get_arg -root $CWAROOT) 93 | echo -e "check process cwagent with pidfile $root/logs/cwagent.pid start program = \"$BKJS_BIN ec2-cwagent-start\" as uid $BKJS_USER with timeout 60 seconds stop program = \"/usr/bin/pkill -f $CWAGENT\"" > /etc/monit.d/cwagent.conf 94 | exit 95 | ;; 96 | 97 | esac 98 | -------------------------------------------------------------------------------- /tools/bkjs-ecr: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Nov 2021 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|ecr-help) 10 | echo "" 11 | echo " ecr-get [-account ID] [-region R] [-login] - login into ECR if required or return ECR host name" 12 | echo " ecr-push -repo REPO -image IMAGE [-tag latest|TAG|VERSION] [-latest] [-create] [-login 0|1] - tag and push a docker image to ECR, create if needed, -latest also pushes it as :latest if -tag is different" 13 | echo " ecr-tag -repo REPO -image IMAGE [-ecr ECR] [-tag latest] - tag and push a docker image to ECR, repository must exist" 14 | echo " ecr-create -repo REPO [-region R] - create a repo if not exist" 15 | echo " ecr-del -tag NAME -repo REPO - delete an image from repo by tag" 16 | echo " ecr-manifest -tag NAME -repo REPO -arm ARMTAG -amd AMDTAG - combine ARM and AMD platforms into one container using manifest" 17 | ;; 18 | 19 | ecr-get) 20 | id=$(get_arg -account $BKJS_ECR_ACCOUNT) 21 | [ -z $id ] && id=$($BKJS_BIN ec2-account) 22 | region=$(get_arg -region $BKJS_ECR_REGION) 23 | [ -z $region ] && region=$($BKJS_BIN ec2-region) 24 | 25 | ecr="$id.dkr.ecr.$region.amazonaws.com" 26 | echo $ecr 27 | 28 | login=$(get_flag -login ${BKJS_ECR_LOGIN:-1}) 29 | [ "$login" != "1" ] && exit 0 30 | 31 | aws ecr get-login-password| docker login --username AWS --password-stdin $ecr 32 | [ "$?" != "0" ] && exit 1 33 | exit 0 34 | ;; 35 | 36 | ecr-push) 37 | repo=$(get_arg -repo) 38 | image=$(get_arg -image) 39 | [ "$repo" = "" -o "$image" = "" ] && echo "-repo and -image are required" && exit 1 40 | 41 | ecr=$($BKJS_BIN ecr-get $(get_all_args)) 42 | [ "$?" != "0" ] && exit 1 43 | 44 | if [ -n "$(get_flag -create)" ]; then 45 | $BKJS_BIN ecr-create -repo $repo 46 | [ "$?" != "0" ] && exit 1 47 | fi 48 | 49 | $BKJS_BIN ecr-tag -ecr $ecr $(get_all_args) 50 | [ "$?" != "0" ] && exit 1 51 | 52 | if [ -n "$(get_flag -latest)" -a "$(get_arg -tag)" != "latest" ]; then 53 | $BKJS_BIN ecr-tag $(get_all_args "-tag") -ecr $ecr 54 | [ "$?" != "0" ] && exit 1 55 | fi 56 | exit 0 57 | ;; 58 | 59 | ecr-tag) 60 | repo=$(get_arg -repo) 61 | image=$(get_arg -image) 62 | [ "$repo" = "" -o "$image" = "" ] && echo "-repo and -image are required" && exit 1 63 | tag=$(get_arg -tag latest) 64 | 65 | ecr=$(get_arg -ecr) 66 | if [ -z "$ecr" ]; then 67 | ecr=$($BKJS_BIN ecr-get $(get_all_args)) 68 | [ "$?" != "0" ] && exit 1 69 | fi 70 | 71 | docker tag $image $ecr/$repo:$tag 72 | docker push $ecr/$repo:$tag 73 | exit 74 | ;; 75 | 76 | ecr-create) 77 | repo=$(get_arg -repo) 78 | [ "$repo" = "" ] && echo "-repo is required" && exit 1 79 | region=$(get_arg -region $BKJS_ECR_REGION) 80 | [ -z $region ] && region=$($BKJS_BIN ec2-region) 81 | 82 | aws ecr describe-repositories --region $region --repository-names $repo --query repositories[*].repositoryUri --output text 2>/dev/null 83 | if [ "$?" != "0" ]; then 84 | aws ecr create-repository --region $region --repository-name $repo 85 | [ "$?" != "0" ] && exit 1 86 | fi 87 | exit 0 88 | ;; 89 | 90 | ecr-del) 91 | repo=$(get_arg -repo) 92 | tag=$(get_arg -tag) 93 | [ "$repo" = "" -o $tag == "" ] && echo "-repo and -tag are required" && exit 1 94 | 95 | aws ecr batch-delete-image --repository-name $repo --image-ids imageTag=$tag 96 | exit 97 | ;; 98 | 99 | ecr-manifest) 100 | repo=$(get_arg -repo) 101 | tag=$(get_arg -tag) 102 | arm64=$(get_arg -arm64) 103 | amd64=$(get_arg -amd64) 104 | [ "$repo" = "" -o $tag == "" ] && echo "-repo and -tag are required" && exit 1 105 | [ "$arm64" = "" -o $amd64 == "" ] && echo "-arm64 and -amd64 are required" && exit 1 106 | 107 | set -e 108 | docker manifest create $repo:$tag $repo:$arm64 $repo:$amd64 109 | docker manifest annotate --arch arm64 $repo:$tag $repo:$arm64 110 | docker manifest annotate --arch amd64 $repo:$tag $repo:$amd64 111 | docker manifest inspect $repo:$tag 112 | docker manifest push $repo:$tag 113 | exit 114 | ;; 115 | 116 | esac 117 | -------------------------------------------------------------------------------- /tools/bkjs-ecs-agent: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Aug 2024 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|docker-help) 10 | echo "" 11 | echo " ecs-setup-agent - setup ECS agent to start in the current runlevel later, updates iptables" 12 | echo " ecs-start-agent - start ECS agent as a docker container if not running, start docker if not running" 13 | echo " ecs-stop-agent - stop ECS agent" 14 | ;; 15 | 16 | ecs-setup-agent) 17 | sysctl -w net.ipv4.conf.all.route_localnet=1 18 | iptables -t nat -A PREROUTING -p tcp -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679 19 | iptables -t nat -A OUTPUT -d 169.254.170.2 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 51679 20 | 21 | mkdir -p /etc/ecs/ /var/log/ecs /var/lib/ecs/data 22 | touch /etc/ecs/ecs.config 23 | 24 | conf=/etc/monit.d/ecs-agent.conf 25 | if [ ! -f $conf ]; then 26 | msg Setup ECS agent... 27 | 28 | $BKJS_BIN setup-rsyslog-docker -tag ID 29 | killall -HUP rsyslogd 30 | 31 | mkdir -p /etc/monit.d 32 | cycles=$(get_arg -cycles 2) 33 | cycles2=$(( $cycles*2 )) 34 | echo "check program ecs-agent with path \"$BKJS_BIN ecs-start-agent\" every $cycles cycles" >$conf 35 | echo " if status != 0 for 2 times within ${cycles2} cycles then alert" >> $conf 36 | 37 | rc-update add docker 38 | fi 39 | exit 40 | ;; 41 | 42 | ecs-start-agent) 43 | if ! rc-service docker status; then 44 | rc-service docker start 45 | exit 0 46 | fi 47 | 48 | if docker top ecs-agent; then 49 | exit 0 50 | fi 51 | 52 | cluster=$(imds meta-data/tags/instance/ECS_CLUSTER) 53 | 54 | exec docker run -d --name ecs-agent \ 55 | --restart=unless-stopped \ 56 | --volume=/var/run:/var/run \ 57 | --volume=/var/log/ecs/:/log \ 58 | --volume=/var/lib/ecs/data:/data \ 59 | --volume=/etc/ecs:/etc/ecs \ 60 | --volume=/sbin:/host/sbin \ 61 | --volume=/lib:/lib \ 62 | --volume=/usr/lib:/usr/lib \ 63 | --volume=/proc:/host/proc \ 64 | --volume=/sys/fs/cgroup:/sys/fs/cgroup \ 65 | --net=host \ 66 | --env-file=/etc/ecs/ecs.config \ 67 | --env=ECS_CLUSTER=${cluster:-default} \ 68 | --env=ECS_LOGFILE=/log/ecs-agent.log \ 69 | --env=ECS_DATADIR=/data \ 70 | --env=ECS_ENABLE_TASK_IAM_ROLE=true \ 71 | --env=ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true \ 72 | --env=ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs","syslog","none"]' \ 73 | --env=ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE=true \ 74 | --cap-add=sys_admin \ 75 | --cap-add=net_admin \ 76 | public.ecr.aws/ecs/amazon-ecs-agent:latest 77 | ;; 78 | 79 | ecs-stop-agent) 80 | exec docker rm -f ecs-agent 81 | ;; 82 | 83 | esac 84 | -------------------------------------------------------------------------------- /tools/bkjs-get: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$BKJS_CMD" in 4 | 5 | help|get-help) 6 | echo "" 7 | echo " get-json - read JSON from the input and show it nicely formatted" 8 | echo " get-jsval FILE PROP [DFLT] [realpath] - return a value from a JSON file by property name" 9 | ;; 10 | 11 | get-json) 12 | exec node -e "console.log(util.inspect(JSON.parse(fs.readFileSync(0).toString()),null,null))" 13 | ;; 14 | 15 | get-jsval) 16 | echo $(get_json_flat "$BKJS_ARGV0" "$BKJS_ARGV1" "$BKJS_ARGV2" "$BKJS_ARGV3") 17 | exit 18 | ;; 19 | 20 | esac 21 | 22 | -------------------------------------------------------------------------------- /tools/bkjs-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|install-help) 10 | echo "" 11 | echo " install-node [-prefix PATH] [-force] [-clean] [-tgz TGZ] - install binary release of the node into $BKJS_HOME or specified path" 12 | ;; 13 | 14 | install-node) 15 | if [ -n "$(get_flag -force)" -a -f $BKJS_HOME/bin/node ]; then 16 | echo "Uninstalling node from $BKJS_HOME ..." 17 | rm -rf $BKJS_HOME/bin/node $BKJS_HOME/bin/npm $BKJS_HOME/bin/npx $BKJS_HOME/lib/node_modules/npm $BKJS_HOME/include/node 18 | [ -n "$(get_flag -clean)" ] && rm -rf $BKJS_HOME/lib/node_modules 19 | fi 20 | [ -f $BKJS_HOME/bin/node ] && echo "already installed as $BKJS_HOME/bin/node" && exit 1 21 | 22 | mkdir -p $BKJS_HOME 23 | [ "$?" != "0" ] && exit "echo failed to create $BKJS_HOME" && exit 1 24 | echo "Installing node into $BKJS_HOME ..." 25 | 26 | tgz=$(get_arg -tgz) 27 | if [ -n "$tgz" ]; then 28 | tar -C $BKJS_HOME --strip-components=1 -xzf $tgz 29 | [ "$?" != "0" ] && exit 1 30 | else 31 | ver=$(get_arg -version v22.15.0) 32 | [ "$OS_ARCH" = "amd64" ] && arch=x64 || arch=$OS_ARCH 33 | platform=$(to_lower $PLATFORM) 34 | curl -L -o node.tgz https://nodejs.org/dist/$ver/node-$ver-$platform-$arch.tar.gz 35 | [ "$?" != "0" ] && exit 1 36 | tar -C $BKJS_HOME --strip-components=1 -xzf node.tgz 37 | rm -rf node.tgz 38 | fi 39 | mv $BKJS_HOME/README.md $BKJS_HOME/LICENSE $BKJS_HOME/CHANGELOG.md $BKJS_HOME/share/doc 40 | exit 41 | ;; 42 | 43 | esac 44 | -------------------------------------------------------------------------------- /tools/bkjs-monit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|monit-help) 10 | echo "" 11 | echo " monit-init-system - setup system monitoring with monit, CPU, disk" 12 | echo " monit-init-bkjs - setup monit to keep bkjs service running without using any other services and monitor" 13 | echo " monit-stop-bkjs - remove and stop bkjs, reload monit" 14 | echo " monit-init-alerts - setup monit mail alerts" 15 | echo " monit-init -name NAME [-gid G] [-timeout 30] [-cycles N] -start SCRIPT -stop SCRIPT - generate a monit service config" 16 | ;; 17 | 18 | monit-init-system) 19 | interval=$(get_arg -interval 15) 20 | delay=$(get_arg -delay 0) 21 | load=$(get_arg -load 7) 22 | lcycles=$(get_arg -lcycles 4) 23 | space=$(get_arg -space 90) 24 | fscycles=$(get_arg -fscycles 50) 25 | path=$(get_arg -path /) 26 | mkdir -p /etc/monit.d 27 | if [ -z "$(egrep -Es '^include /etc/monit.d' /etc/monitrc)" ]; then 28 | echo 'include /etc/monit.d/*' >> /etc/monitrc 29 | fi 30 | echo "set logfile syslog" > /etc/monit.d/system.conf 31 | echo "set daemon $interval with start delay $delay" > /etc/monit.d/system.conf 32 | echo "check system \$HOST every $lcycles cycles if loadavg(5min) > $load then alert" >> /etc/monit.d/system.conf 33 | echo "check filesystem rootfs with path $path every $fscycles cycles if space usage > ${space}% then alert" >> /etc/monit.d/system.conf 34 | exit 35 | ;; 36 | 37 | monit-start-instance) 38 | file=~/.monit.uptime 39 | uptime=$(stat -c %Z /proc/1/cmdline) 40 | [ -f $file -a "$(cat $file)" = "$uptime" ] && exit 41 | echo $uptime > $file 42 | run_bkjs_cmd start-hook 43 | exit 44 | ;; 45 | 46 | monit-init-start-instance) 47 | bin=$(get_arg -bin $BKJS_BIN) 48 | mkdir -p /etc/monit.d 49 | echo -e "check program start-instance with path \"$bin monit-start-instance\" if status > -1 then unmonitor" > /etc/monit.d/start-instance.conf 50 | exit 51 | ;; 52 | 53 | monit-init-bkjs) 54 | timeout=$(get_arg -timeout 30) 55 | bin=$(get_arg -bin $BKJS_BIN) 56 | echo -e "check process bkjs with pidfile \"$BKJS_HOME/var/master.pid\" start program = \"$bin run-master $(get_all_args)\" as uid $BKJS_USER with timeout $timeout seconds stop program = \"$bin stop\"" > /etc/monit.d/bkjs.conf 57 | exit 58 | ;; 59 | 60 | monit-stop-bkjs) 61 | rm -f /etc/monit.d/bkjs.conf 62 | killall -HUP monit 63 | $BKJS_BIN stop 64 | exit 65 | ;; 66 | 67 | monit-init-alerts) 68 | [ -n "$(get_flag -force)" ] && rm -f /etc/monit.d/alert.conf 69 | [ -f /etc/monit.d/alert.conf ] && exit 70 | email=$(get_arg -email) 71 | [ -z "$email" ] && exit 72 | user=$(get_arg -user) 73 | host=$(get_arg -host) 74 | password=$(get_arg -password) 75 | events=$(get_arg -events "action,connection,data,pid,ppid,exec,content,resource,status,timeout") 76 | echo "Init monit alert: $email $events, $host, $user" 77 | [ "$events" != "" ] && events="only on { $events }" 78 | echo -e "set alert $email $events" > /etc/monit.d/alert.conf 79 | echo -e "set mail-format { from: $email }" >> /etc/monit.d/alert.conf 80 | [ -z "$host" ] && exit 81 | server="set mailserver $host" 82 | if match $host amazonaws; then server="$server port 465"; fi 83 | [ -n "$user" ] && server="$server username $user" 84 | [ -n "$password" ] && server="$server password $password" 85 | if match $host amazonaws; then server="$server using tlsv13"; fi 86 | echo -e $server >> /etc/monit.d/alert.conf 87 | exit 88 | ;; 89 | 90 | monit-init) 91 | name=$(get_arg -name) 92 | start=$(get_arg -start) 93 | stop=$(get_arg -stop) 94 | [ "$name" = "" -o "$start" = "" -o "$stop" = "" ] && echo "invalid init-monit arguments" && exit 95 | timeout=$(get_arg -timeout 30) 96 | cycles=$(get_arg -cycles) 97 | [ -n "$cycles" ] && cycles="for $cycles cycles" 98 | gid=$(get_arg -gid) 99 | [ -n "$gid" ] && gid="and gid $gid" 100 | echo -e "check process $name with pidfile \"$BKJS_HOME/var/$name.pid\" start program = \"$start\" as uid $BKJS_USER $gid with timeout $timeout seconds $cycles stop program = \"$stop\"" > /etc/monit.d/$name.conf 101 | exit 102 | ;; 103 | 104 | esac 105 | -------------------------------------------------------------------------------- /tools/bkjs-nats: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|nats-help) 10 | echo "" 11 | echo " nats-get - install local NATS server in $BKJS_HOME/bin" 12 | echo " nats-getcli - install NATS command line tool in $BKJS_HOME/bin" 13 | echo " nats-run - run local NATS server installed in $BKJS_HOME/bin" 14 | echo " nats-stop - stop local NATS server" 15 | ;; 16 | 17 | nats-get) 18 | platform=$(to_lower $PLATFORM) 19 | curl -L -o /tmp/nats.tgz https://github.com/nats-io/nats-server/releases/download/v2.9.15/nats-server-v2.9.15-$platform-$OS_ARCH.tar.gz 20 | tar --strip-components=1 -C /tmp -xzf /tmp/nats.tgz 21 | mv /tmp/nats-server $BKJS_HOME/bin 22 | rm -rf /tmp/nats.tgz 23 | if [ ! -f $BKJS_HOME/etc/nats.conf ]; then 24 | echo 'listen: localhost:4222' >> $BKJS_HOME/etc/nats.conf 25 | echo 'http: localhost:8222' >> $BKJS_HOME/etc/nats.conf 26 | echo 'syslog: true' >> $BKJS_HOME/etc/nats.conf 27 | echo 'logtime: false' >> $BKJS_HOME/etc/nats.conf 28 | echo "pid_file: $BKJS_HOME/var/nats.pid" >> $BKJS_HOME/etc/nats.conf 29 | echo 'jetstream: {' >> $BKJS_HOME/etc/nats.conf 30 | echo " store_dir: \"$BKJS_HOME/var\"" >> $BKJS_HOME/etc/nats.conf 31 | echo ' max_file: 1G' >> $BKJS_HOME/etc/nats.conf 32 | echo '}' >> $BKJS_HOME/etc/nats.conf 33 | echo 'cluster {' >> $BKJS_HOME/etc/nats.conf 34 | echo ' name: nats' >> $BKJS_HOME/etc/nats.conf 35 | echo ' #listen: 0.0.0.0:6222' >> $BKJS_HOME/etc/nats.conf 36 | echo ' routes: [' >> $BKJS_HOME/etc/nats.conf 37 | echo ' nats-route://nats:6222' >> $BKJS_HOME/etc/nats.conf 38 | echo ' ]' >> $BKJS_HOME/etc/nats.conf 39 | echo '}' >> $BKJS_HOME/etc/nats.conf 40 | fi 41 | exit 42 | ;; 43 | 44 | nats-getcli) 45 | platform=$(to_lower $PLATFORM) 46 | curl -L -o /tmp/nats.zip https://github.com/nats-io/natscli/releases/download/v0.0.35/nats-0.0.35-$platform-$OS_ARCH.zip 47 | unzip -j /tmp/nats.zip -d /tmp '*/nats' 48 | mv /tmp/nats $BKJS_HOME/bin 49 | rm -rf /tmp/nats.zip 50 | exit 51 | ;; 52 | 53 | nats-run) 54 | name=$(get_arg -name $(uname -n|cut -f1 -d.)) 55 | mkdir -p $BKJS_HOME/var $BKJS_HOME/log 56 | exec nohup nats-server -n $name -c $BKJS_HOME/etc/nats.conf >>$BKJS_HOME/log/message.log 2>&1 & 57 | exit 58 | ;; 59 | 60 | nats-stop) 61 | pkill -f nats 62 | exit 63 | ;; 64 | 65 | nats-init-monit) 66 | $0 monit-init -name nats -start "$BKJS_BIN nats-run" -stop "$BKJS_BIN nats-stop" 67 | exit 68 | ;; 69 | 70 | esac 71 | 72 | -------------------------------------------------------------------------------- /tools/bkjs-redis: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|redis-help) 10 | echo "" 11 | echo " redis-get - install Redis server into $BKJS_HOME" 12 | echo " redis-init - install and setup Redis server to be run on start and to be monitored (Linux only)" 13 | echo " redis-run [-memsize PERCENT] [-memmax SIZE] [-slave-host HOST] - run local Redis server, uses config file $BKJS_HOME/etc/redis.conf" 14 | echo " redis-stop - stop local Redis server" 15 | echo " redis-init-monit [-memsize PERCENT] [-memmax SIZE] - setup Redis server to be run on start and to be monitored (Linux only)" 16 | echo "" 17 | ;; 18 | 19 | redis-init) 20 | ($0 redis-get $(get_all_args)) 21 | ($0 redis-run $(get_all_args)) 22 | if [ "$PLATFORM" = "Linux" ]; then 23 | sudo $BKJS_BIN redis-init-monit 24 | fi 25 | exit 26 | ;; 27 | 28 | redis-get) 29 | # Install redis server 30 | if [ -f $BKJS_HOME/bin/redis-server ]; then 31 | [ "$(get_flag -force)" = "" ] && exit 32 | echo "Uninstalling redis from $BKJS_HOME..." 33 | rm -f $BKJS_HOME/bin/redis-* 34 | cp $BKJS_HOME/etc/redis.local.conf $BKJS_HOME/etc/redis.local.conf.old 35 | fi 36 | 37 | ver=$(get_arg -version 7.2.4) 38 | curl -L -o redis.tgz http://download.redis.io/releases/redis-$ver.tar.gz 39 | 40 | mkdir -p redis $BKJS_HOME/etc 41 | tar -C redis --strip-components=1 -xzf redis.tgz 42 | 43 | [ -n "$(get_flag -tls)" ] && tls="BUILD_TLS=yes" 44 | make -C redis $tls install PREFIX=$BKJS_HOME 45 | 46 | cp redis/redis.conf $BKJS_HOME/etc 47 | rm -rf redis redis.tgz 48 | $BKJS_BIN redis-config 49 | exit 50 | ;; 51 | 52 | redis-config) 53 | conf=$BKJS_HOME/etc/redis.conf 54 | [ ! -f $conf ] && conf=/etc/redis.conf 55 | local=$BKJS_HOME/etc/redis.local.conf 56 | 57 | if [ -z "$(grep -s $local $conf)" ]; then 58 | echo "include $local" >> $conf 59 | fi 60 | 61 | echo 'syslog-enabled yes' > $local 62 | echo "dir $BKJS_HOME/var/" >> $local 63 | echo "timeout 3600" >> $local 64 | echo "bind *" >> $local 65 | echo "protected-mode no" >> $local 66 | echo "unixsocket $BKJS_HOME/var/redis.sock" >> $local 67 | echo "pidfile $BKJS_HOME/var/redis.pid" >> $local 68 | echo "logfile $BKJS_HOME/log/redis.log" >> $local 69 | echo "tcp-keepalive 60" >> $local 70 | echo "maxmemory-policy volatile-lru" >> $local 71 | echo 'daemonize yes' >> $local 72 | 73 | if [ "$(whoami)" = "root" ]; then 74 | [ -n "$BKJS_USER" ] && chown $BKJS_USER $local 75 | 76 | if [ "$PLATFORM" = "Linux" ]; then 77 | echo 1 > /proc/sys/vm/overcommit_memory 78 | echo never > /sys/kernel/mm/transparent_hugepage/enabled 79 | fi 80 | fi 81 | exit 82 | ;; 83 | 84 | redis-run) 85 | # Percent from the total memory 86 | memsize=$(get_arg -memsize) 87 | [ "$memsize" != "" ] && memmax="$(( ($(free -m|grep Mem:|awk '{print $2}') * $memsize) / 100 ))mb" 88 | memmax=$(get_arg -memmax $memmax) 89 | if [ "$memmax" != "" ]; then 90 | conf=$BKJS_HOME/etc/redis.local.conf 91 | if [ -z "$(grep -s "maxmemory $memmax" $conf)" ]; then 92 | echo "maxmemory $memmax" >> $conf 93 | fi 94 | fi 95 | 96 | conf=$BKJS_HOME/etc/redis.conf 97 | [ ! -f $conf ] && conf=/etc/redis.conf 98 | 99 | touch $BKJS_HOME/log/redis.log 100 | redis-server $conf 101 | 102 | slavehost=$(get_arg -slave-host) 103 | slaveport=$(get_arg -slave-port 6379) 104 | if [ "$slavehost" != "" ]; then 105 | redis-cli slaveof $slavehost $slaveport 106 | fi 107 | exit 108 | ;; 109 | 110 | redis-stop) 111 | pkill -f redis-server 112 | exit 113 | ;; 114 | 115 | redis-init-monit) 116 | echo -e "$BKJS_HOME/log/redis.log {\n weekly\n rotate 10\n copytruncate\n delaycompress\n compress\n notifempty\n missingok\n}" > /etc/logrotate.d/redis 117 | echo -e "check process redis-server with pidfile \"$BKJS_HOME/var/redis.pid\" start program = \"$BKJS_BIN redis-run $(get_all_args)\" as uid $BKJS_USER stop program = \"$BKJS_BIN redis-stop\" if failed host 127.0.0.1 port 6379 for 2 cycles then restart" > /etc/monit.d/redis.conf 118 | exit 119 | ;; 120 | 121 | esac 122 | 123 | -------------------------------------------------------------------------------- /tools/bkjs-sync: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|sync-help) 10 | echo 11 | echo " sync [-host HOST] [-path PATH] [-del] [-user USER] [-ssh-key pem] [-ssh OPTS] [-exclude PATTERN] [-bkcmd CMD] - push the backend code to the remote host using rsync, default path is ~/node_modules" 12 | ;; 13 | 14 | sync) 15 | # Put backend code to the remote site 16 | host=$(get_arg -host $BKJS_HOST) 17 | [ "$host" = "" ] && echo "no sync without -host" && exit 18 | mod=$(get_json package.json name) 19 | [[ -z "$mod" ]] && echo "no sync without package.json" && exit 1 20 | path=$(get_arg -path) 21 | [ -z "$path" ] && path=$(get_json package.json config.sync.path) 22 | [ -z "$path" ] && echo "no sync without -path or config.sync.path" && exit 1 23 | sshargs=$(concat_arg -ssh $BKJS_SSH_ARGS) 24 | user=$(get_arg -user) 25 | [ "$user" != "" ] && sshargs="$sshargs -l $user" 26 | key=$(get_arg -ssh-key) 27 | [ "$key" != "" -a -f $HOME/.ssh/$key.pem ] && sshargs="$sshargs -i $HOME/.ssh/$key.pem -o IdentitiesOnly=yes" 28 | rsyncargs=$(concat_arg -rsync $BKJS_RSYNC_ARGS) 29 | bkcmd=$(get_arg -bkcmd) 30 | if [ "$bkcmd" != "" ]; then 31 | bkcmd="--rsync-path=/home/$BKJS_USER/bin/bkrsync -bkcmd $(echo $bkcmd|sed 's/ /%20/g')" 32 | else 33 | bkcmd=-a 34 | fi 35 | include=$(get_json package.json config.sync.include) 36 | for inc in $include; do 37 | rsyncargs="$rsyncargs --include=$inc" 38 | done 39 | if [ -f .gitignore ]; then 40 | rsyncargs="$rsyncargs --exclude-from .gitignore" 41 | fi 42 | [ -f $HOME/.gitignore_global ] && rsyncargs="$rsyncargs --exclude-from $HOME/.gitignore_global" 43 | exclude=$(get_json package.json config.sync.exclude) 44 | [[ -n "$exclude" ]] && rsyncargs="$rsyncargs --exclude=$exclude" 45 | [ "$(get_flag -del)" != "" ] && rsyncargs="$rsyncargs --del" 46 | echo "Deploying the module $mod: ssh $sshargs $rsyncargs $bkcmd to $host:$path/$mod" 47 | [ -n "$(get_flag -dry-run)" ] && exit 0 48 | for h in $host; do 49 | rsync -av -e "ssh $sshargs" "$bkcmd" $rsyncargs . $h:$path/$mod 50 | done 51 | exit 52 | ;; 53 | 54 | esac 55 | -------------------------------------------------------------------------------- /tools/bkjs-test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$BKJS_CMD" in 4 | 5 | help|test-help) 6 | echo "" 7 | echo " test-all [-path P] [-skip F] [-filter F] [-log L] - run all tests in the local tests/ folder, skip/filter control which files to run" 8 | echo " test-packages [-path P] [-skip F] [-filter F] [-test-config C] - run all tests for all packages, skip/filter can control what to run/skip" 9 | echo " test-FILE [-path P] [-test NAME] [-test-config C] - run a test function test-NAME in the script file tests/FILE.js" 10 | ;; 11 | 12 | test-all) 13 | nolog=-no-log-filter 14 | log=$(get_arg -log none) 15 | [ $log != none ] && nolog="" 16 | skip=$(get_arg -skip) 17 | filter=$(get_arg -filter) 18 | path=$(get_arg -path) 19 | [ "$path" != "" ] && cd $path 20 | test=$(get_arg -test .) 21 | files=$(find tests -name '*.js'|sort) 22 | [ "$?" != "0" ] && exit 1 23 | err=0 24 | for file in $files; do 25 | if match $file tests/_; then continue; fi 26 | fn=$path/$file 27 | if match $fn $skip; then continue; fi 28 | if [[ -n "$filter" ]] && ! match $fn $filter; then continue; fi 29 | $BKJS_BIN test-$(basename $file .js) -test $test -log $log $nolog $(get_all_args "-log -path -skip -filter -test") 30 | [ "$?" != "0" ] && err=1 31 | done 32 | exit $err 33 | ;; 34 | 35 | test-packages) 36 | path=$(get_arg -path .) 37 | dirs=$(find "$path" -name tests -type d|sort) 38 | [ "$?" != "0" ] && exit 1 39 | err=0 40 | for d in $dirs; do 41 | ($BKJS_BIN test-all -path $(dirname $d) $(get_all_args "-path")) 42 | [ "$?" != "0" ] && err=1 43 | done 44 | exit $err 45 | ;; 46 | 47 | test-args) 48 | echo 'usage: bkjs test-args -skip " -a" -a -b b -flag -arg' 49 | echo 'result: "all: -b b -flag -arg arg: dflt flag: 1 skip: -a"' 50 | echo 51 | echo "all: $(get_all_args "-skip $(get_arg -skip)") arg: $(get_arg -arg dflt) flag: $(get_flag -flag) skip: $(get_arg -skip)" 52 | exit 53 | ;; 54 | 55 | test-*) 56 | file=$(echo $BKJS_CMD | sed 's/^test-//') 57 | [ -n "$(get_flag -dry-run)" ] && echo "$0 $(pwd)/$file.js $(get_all_args)" && exit 0 58 | tests=$(pwd)/tests 59 | [ ! -f tests/$file.js ] && echo "$tests/$file.js is not found" && exit 1 60 | test=$(get_arg -test $file) 61 | config=$(get_arg -test-config) 62 | exec $BKJS_BIN shell -test-config $config,$tests/config,$tests/config.$file -test-file $tests/$file.js -run-ipc -run-api -run-worker $(get_all_args "-test -test-config") -test-run $test 63 | ;; 64 | 65 | esac 66 | 67 | -------------------------------------------------------------------------------- /tools/docker/Dockerfile.abuild: -------------------------------------------------------------------------------- 1 | ARG ALPINE=3.21 2 | 3 | FROM alpine:$ALPINE 4 | 5 | RUN apk --no-cache add \ 6 | alpine-conf \ 7 | alpine-sdk \ 8 | apk-tools \ 9 | coreutils \ 10 | cmake \ 11 | doas-sudo-shim \ 12 | ccache \ 13 | mc \ 14 | nodejs \ 15 | npm \ 16 | python3 \ 17 | go \ 18 | curl \ 19 | file \ 20 | zip \ 21 | rsync \ 22 | zlib-dev \ 23 | zimg-dev \ 24 | fontconfig-dev \ 25 | freetype-dev \ 26 | imlib2-dev \ 27 | nasm && \ 28 | apk -U upgrade -a 29 | 30 | RUN adduser -D alpine && \ 31 | addgroup alpine abuild && \ 32 | echo 'alpine ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers && \ 33 | node-gyp install 34 | -------------------------------------------------------------------------------- /tools/endpoints.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // Jan 2018 4 | // 5 | 6 | var fs = require('fs') 7 | var path = require("path"); 8 | 9 | var files = fs.readdirSync(".").filter(function(x) { return fs.statSync(x).isFile() && x.match(/\.js$/); }); 10 | 11 | try { 12 | files = files.concat(fs.readdirSync("lib/").filter(function(x) { return fs.statSync("lib/" + x).isFile() && x.match(/\.js$/); }).map(function(x) { return "lib/" + x })); 13 | } catch (e) {} 14 | try { 15 | files = files.concat(fs.readdirSync("modules/").filter(function(x) { return fs.statSync("modules/" + x).isFile() && x.match(/\.js$/); }).map(function(x) { return "modules/" + x })); 16 | } catch (e) {} 17 | 18 | var text = ""; 19 | 20 | files.forEach(function(file) { 21 | if (process.argv.length > 2 && !file.match(process.argv[2])) return; 22 | var state, pos; 23 | var data = fs.readFileSync(file).toString().split("\n"); 24 | for (var i = 0; i < data.length; i++) { 25 | var line = data[i]; 26 | if (!line) continue; 27 | 28 | // express endpoint 29 | var d = line.match(/^ +api.app.(all|get|post)/); 30 | if (d) { 31 | text += "\n" + path.basename(file, '.js') + "\n " + line.replace(/(^[^/]+|, function.+)/g, "").replace(/\\\//g, "/") + "\n "; 32 | state = 1; 33 | continue; 34 | } 35 | // switch 36 | d = line.match(/^( +)switch \((req.params|cmd)/); 37 | if (d && state == 1) { 38 | state = 2; 39 | pos = d[1].length; 40 | continue; 41 | } 42 | // other switch 43 | d = line.match(/^( +)switch \(/); 44 | if (d && state == 2) { 45 | state = d[1].length; 46 | continue; 47 | } 48 | // end switch 49 | d = line.match(/^( +)}$/); 50 | if (d && state > 2 && state == d[1].length) { 51 | state = 2; 52 | continue; 53 | } 54 | // case 55 | d = line.match(/^ +case ["']/); 56 | if (d && state == 2) { 57 | text += line.replace(/case |['":]/g, "").trim() + " "; 58 | continue; 59 | } 60 | // default, end of switch 61 | d = line.match(/^( +)default:/); 62 | if (d && state && pos == d[1].length) { 63 | state = 0; 64 | text += "\n"; 65 | } 66 | d = line.match(/^[a-zA-Z0-9._] = function/); 67 | if (d && state) { 68 | state = 0; 69 | text += "\n"; 70 | } 71 | } 72 | }); 73 | 74 | console.log(text); 75 | process.exit(0); 76 | 77 | -------------------------------------------------------------------------------- /tools/locales.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // Jan 2017 4 | // 5 | 6 | var fs = require('fs') 7 | var path = require("path"); 8 | var bkjs = require('backendjs'); 9 | var lib = bkjs.lib; 10 | 11 | var paths = ["."]; 12 | for (var i = 2; i < process.argv.length; i++) { 13 | if (process.argv[i][0] != "-") paths.push(process.argv[i]); 14 | } 15 | 16 | var files = []; 17 | for (var i in paths) { 18 | files = files.concat(lib.findFileSync(paths[i], { depth: 2, types: "f", include: /\.js$/, exclude: /(tools|tests)\// })) 19 | } 20 | var msgs = {}; 21 | var locale = {}; 22 | var lang = lib.getArg("-lang"); 23 | if (lang) locale = JSON.parse(fs.readFileSync("locales/" + lang + ".json")); 24 | 25 | var rx = [ 26 | /api.sendReply\(res,[ 0-9]+, ?"([^\"]+)"/, 27 | /message: ?"([^\"]+)"/, 28 | /[_ ]msg: ?"([^\"]+)"/, 29 | /__\("([^""]+)"/, 30 | /phrase: ?"([^\"]+)"/, 31 | /"([^\"@]*@[^\"@]+@[^\"]*)"/, 32 | ]; 33 | files.forEach(function(file) { 34 | var doc = ""; 35 | var data = fs.readFileSync(file).toString().split("\n"); 36 | for (var i = 0; i < data.length; i++) { 37 | var line = data[i].trim(); 38 | if (!line || line[0] == "/") continue; 39 | // Skip config help 40 | if (/{ *name:.+descr:/.test(line)) continue; 41 | for (var j in rx) { 42 | var d = line.match(rx[j]); 43 | if (d) { 44 | // Single placeholder 45 | if (/^[^a-z]*@[a-z0-9_]+@[^a-z]*$/i.test(d[1])) continue; 46 | msgs[d[1]] = locale[d[1]] || ""; 47 | break; 48 | } 49 | } 50 | } 51 | }); 52 | if (lib.isArg("-merge")) { 53 | for (var p in locale) if (!msgs[p]) msgs[p] = locale[p]; 54 | } 55 | console.log(JSON.stringify(msgs, null, " ")); 56 | process.exit(0); 57 | 58 | -------------------------------------------------------------------------------- /web/css/doc.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Avenir Next", Helvetica, Arial, sans-serif; 3 | font-size: 1em; 4 | padding:1em; 5 | margin:auto; 6 | max-width:80%; 7 | background:#fefefe; 8 | } 9 | 10 | h1 { 11 | border-bottom: 1px solid #dbdbdb; 12 | margin: 1.5rem 0 1.5rem 0; 13 | padding-bottom: 7px; 14 | color: #000; 15 | } 16 | 17 | h2 { 18 | border-bottom: 1px solid #dbdbdb; 19 | margin: 1.5rem 0 1.5rem 0; 20 | padding-bottom: 7px; 21 | color: #333; 22 | } 23 | 24 | h3, h4 { 25 | margin: 1.2rem 0 1.2rem 0; 26 | padding-bottom: 5px; 27 | color: #333; 28 | } 29 | 30 | h5 { 31 | margin: 0.7rem 0 0.7rem 0; 32 | padding-bottom: 5px; 33 | color: #333; 34 | } 35 | 36 | h6 { 37 | margin: 0.7rem 0 0.7rem 0; 38 | padding-bottom: 5px; 39 | color: #333; 40 | } 41 | 42 | hr { 43 | height: 0.2em; 44 | border: 0; 45 | color: #CCCCCC; 46 | background-color: #CCCCCC; 47 | } 48 | 49 | p, blockquote, ul, ol, dl, li, table, pre { 50 | margin: 12px 0; 51 | } 52 | 53 | blockquote { 54 | border-left: 4px solid #dddddd; 55 | padding: 0 15px; 56 | color: #777777; 57 | } 58 | 59 | li, ul { 60 | margin: 2px 0; 61 | } 62 | 63 | a, a:visited { 64 | color: #4183C4; 65 | background-color: inherit; 66 | text-decoration: none; 67 | } 68 | 69 | button { 70 | padding: 4px 6px; 71 | border-radius: 5px; 72 | border: 1px solid #bbb; 73 | background-color: #eee; 74 | } 75 | 76 | code, pre { 77 | font-family: Monaco; 78 | border: 0; 79 | border-radius: 0; 80 | } 81 | 82 | pre { 83 | background-color: #F8F8F8; 84 | margin: 2px 0 8px; 85 | padding: 18px; 86 | } 87 | 88 | code { 89 | color: #BB637F; 90 | padding: 4px 4px 2px 0; 91 | letter-spacing: -0.3px; 92 | } 93 | 94 | li p:first-child code:first-child { 95 | font-size: 1.3em; 96 | font-family: "Avenir Next", Helvetica, Arial, sans-serif; 97 | font-weight: 500; 98 | background-color: inherit; 99 | border: inherit; 100 | } 101 | 102 | pre code { 103 | padding: 0; 104 | color: inherit; 105 | white-space: pre-wrap; 106 | background-color: transparent; 107 | } 108 | 109 | -------------------------------------------------------------------------------- /web/css/index.html: -------------------------------------------------------------------------------- 1 | nothing here 2 | 3 | -------------------------------------------------------------------------------- /web/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/img/1.png -------------------------------------------------------------------------------- /web/img/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/img/index.html -------------------------------------------------------------------------------- /web/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/img/loading.gif -------------------------------------------------------------------------------- /web/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/img/logo.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Backend 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/js/app.ko.js: -------------------------------------------------------------------------------- 1 | // 2 | // Knockout plugin for Alpinejs app 3 | // Vlad Seryakov 2024 4 | // 5 | 6 | (() => { 7 | const app = window.app; 8 | const _type = "ko"; 9 | 10 | class Component extends app.Component { 11 | static $type = _type; 12 | 13 | constructor(name, params, componentInfo) { 14 | super(name, params); 15 | this.$type = _type; 16 | this.element = this.$el = componentInfo.element; 17 | } 18 | 19 | dispose() { 20 | super.destroy(); 21 | 22 | // Auto dispose all subscriptions 23 | for (const p in this) { 24 | if (this[p] && typeof this[p] == "object" && 25 | typeof this[p].dispose == "function" && 26 | typeof this[p].disposeWhenNodeIsRemoved == "function") { 27 | this[p].dispose(); 28 | delete this[p]; 29 | } else 30 | if (ko.isComputed(this[p])) { 31 | this[p].dispose(); 32 | delete this[p]; 33 | } 34 | } 35 | for (const i in this.__sub) this.__sub[i].dispose(); 36 | delete this.__sub; 37 | delete this.element; 38 | } 39 | 40 | subscribe(obj, extend, callback) { 41 | if (!ko.isObservable(obj)) return; 42 | if (!this.__sub) this.__sub = []; 43 | if (typeof extend == "function") { 44 | var sub = obj.subscribe(extend); 45 | } else { 46 | if (typeof extend == "number") { 47 | extend = { rateLimit: { method: "notifyWhenChangesStop", timeout: extend } }; 48 | } 49 | sub = obj.extend(extend).subscribe(callback); 50 | } 51 | this.__sub.push(sub); 52 | return sub; 53 | } 54 | } 55 | 56 | function create(name) 57 | { 58 | var tmpl = app.resolve(name); 59 | if (!tmpl) return null; 60 | 61 | return { 62 | name: name, 63 | template: tmpl.template, 64 | viewModel: { 65 | createViewModel: function(params, componentInfo) { 66 | if (typeof tmpl.component != "function") return; 67 | params = componentInfo.element?._x_params || params || {}; 68 | delete componentInfo.element?._x_params; 69 | 70 | if (tmpl.component.$noevents) params.$noevents = 1; 71 | const component = new tmpl.component(name, params, componentInfo); 72 | app.call(component, "init"); 73 | return component; 74 | } 75 | }, 76 | }; 77 | } 78 | 79 | function cleanup(element) 80 | { 81 | app.$empty(element, ko.cleanNode); 82 | ko.cleanNode(element); 83 | app.$attr(element, "data-bind", null); 84 | delete element._x_params; 85 | } 86 | 87 | function dataFor(element) 88 | { 89 | var data; 90 | while (element) { 91 | if ((data = ko.dataFor(element))) return data; 92 | element = element.parentElement; 93 | } 94 | } 95 | 96 | function data(element) 97 | { 98 | if (!app.isE(element)) element = app.$(app.main)?.firstElementChild; 99 | return ko.dataFor(element); 100 | } 101 | 102 | function render(element, options) 103 | { 104 | cleanup(element); 105 | 106 | app.$attr(element, "data-bind", `component: '${options.name}'`); 107 | element._x_params = options.params; 108 | ko.applyBindings(dataFor(element), element); 109 | } 110 | 111 | ko.components.loaders.unshift({ 112 | getConfig: (name, callback) => (callback(create(name))) 113 | }); 114 | 115 | ko.templateEngine.prototype.makeTemplateSource = function(template, doc) { 116 | if (typeof template == "string") { 117 | var elem = (doc || document).getElementById(template); 118 | if (!elem && app.templates[template]) { 119 | elem = ko.utils.parseHtmlFragment(`", doc)[0]; 120 | } 121 | if (!elem) throw new Error("Cannot find template with ID " + template); 122 | return new ko.templateSources.domElement(elem); 123 | } else 124 | if ((template.nodeType == 1) || (template.nodeType == 8)) { 125 | return new ko.templateSources.anonymousTemplate(template); 126 | } 127 | throw new Error("Unknown template type: " + template); 128 | } 129 | 130 | app.plugin(_type, { render, cleanup, data, Component }); 131 | 132 | })(); 133 | -------------------------------------------------------------------------------- /web/js/bkjs-ko.js: -------------------------------------------------------------------------------- 1 | // 2 | // Vlad Seryakov 2014 3 | // 4 | 5 | (() => { 6 | var app = window.app; 7 | 8 | app.koAuth = ko.observable(0); 9 | app.koMobile = ko.observable(); 10 | 11 | // Variable utils 12 | app.koVal = ko.unwrap; 13 | app.isKo = ko.isObservable; 14 | app.isKa = ko.isObservableArray; 15 | app.koRegister = (...args) => args.forEach(x => ko.components.register(x, {})) 16 | 17 | app.koSetObject = function(obj, options) 18 | { 19 | if (!app.isO(obj)) obj = {}; 20 | for (const p in obj) { 21 | if (options[p] !== undefined) continue; 22 | if (ko.isComputed(obj[p])) continue; 23 | if (app.isKo(obj[p])) obj[p](undefined); else obj[p] = undefined; 24 | } 25 | for (const p in options) { 26 | if (ko.isComputed(obj[p])) continue; 27 | if (app.isKo(obj[p])) obj[p](app.koVal(options[p])); else obj[p] = app.koVal(options[p]); 28 | } 29 | return obj; 30 | } 31 | 32 | app.koUpdateObject = function(obj, options) 33 | { 34 | if (!app.isO(obj)) obj = {}; 35 | for (const p in options) { 36 | if (ko.isComputed(obj[p])) continue; 37 | if (app.isKo(obj[p])) obj[p](app.koVal(options[p])); else obj[p] = app.koVal(options[p]); 38 | } 39 | return obj; 40 | } 41 | 42 | app.koConvert = function(obj, name, val, dflt) 43 | { 44 | if (!app.isO(obj)) obj = {}; 45 | if (!app.isKo(obj[name])) { 46 | obj[name] = Array.isArray(val || dflt) ? ko.observableArray(obj[name]) : ko.observable(obj[name]); 47 | } 48 | if (val !== undefined) obj[name](val); 49 | if (dflt !== undefined && !obj[name]()) obj[name](dflt); 50 | return obj; 51 | } 52 | 53 | app.koSetMobile = function() 54 | { 55 | app.koMobile(/xs|sm|md/.test(app.getBreakpoint())) 56 | } 57 | 58 | // Useful bindings 59 | ko.bindingHandlers.hidden = { 60 | update: function (element, valueAccessor) { 61 | var value = ko.utils.unwrapObservable(valueAccessor()); 62 | var isCurrentlyHidden = !(element.style.display == ""); 63 | if (value && !isCurrentlyHidden) element.style.display = "none"; else 64 | if ((!value) && isCurrentlyHidden) element.style.display = ""; 65 | } 66 | }; 67 | 68 | ko.bindingHandlers.html.update = function (element, valueAccessor) { 69 | ko.utils.setHtml(element, app.sanitizer.run(ko.utils.unwrapObservable(valueAccessor()))); 70 | }; 71 | 72 | // Apply KO to all bootpopups 73 | bootpopup.plugins.push({ 74 | before: function(self) { 75 | if (self.options.data) ko.applyBindings(self.options.data, self.modal); 76 | }, 77 | complete: function(event, self) { 78 | if (self.options.data) ko.cleanNode(self.modal); 79 | }, 80 | }); 81 | 82 | app.$ready(() => { 83 | app.koSetMobile(); 84 | app.$on(window, "resize", app.koSetMobile); 85 | app.on("login", () => { app.koAuth(app.loggedIn) }); 86 | app.on("logout", () => { app.koAuth(app.loggedIn) }); 87 | }); 88 | 89 | })(); 90 | -------------------------------------------------------------------------------- /web/js/bkjs-passkey.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2024 4 | // 5 | 6 | // Passkey support 7 | 8 | (() => { 9 | var app = window.app; 10 | 11 | app.passkeyInit = function(callback) 12 | { 13 | if (app.passkeyClient) return; 14 | import("/js/webauthn.min.mjs").then((mod) => { 15 | app.passkeyClient = mod.client; 16 | app.call(callback) 17 | }).catch((err) => { 18 | app.call(callback, err); 19 | }); 20 | } 21 | 22 | app.passkeyRegisterStart = function(options, callback) 23 | { 24 | app.get({ url: "/passkey/register", data: options?.query }, callback); 25 | } 26 | 27 | app.passkeyRegisterFinish = function(config, options, callback) 28 | { 29 | app.passkeyClient.register(options?.name || app.account?.name, config?.challenge, { 30 | attestation: true, 31 | userHandle: config?.id, 32 | domain: config?.domain, 33 | }).then((data) => { 34 | app.sendRequest({ url: "/passkey/register", data: Object.assign(data || {}, options?.query) }, callback); 35 | }).catch((err) => { 36 | app.call(callback, err); 37 | }); 38 | } 39 | 40 | app.passkeyRegister = function(options, callback) 41 | { 42 | app.passkeyRegisterStart(options, (err, config) => { 43 | if (err) return app.call(callback, err); 44 | app.passkeyRegisterFinish(config, options, callback); 45 | }); 46 | } 47 | 48 | app.passkeyLogin = function(options, callback) 49 | { 50 | app.get({ url: "/passkey/login" }, (err, config) => { 51 | if (err) return app.call(callback, err); 52 | 53 | app.passkeyClient.authenticate(app.strSplit(options?.ids), config.challenge, { 54 | domain: config.domain, 55 | }).then((data) => { 56 | app.login({ url: "/passkey/login", data: Object.assign(data, options?.query) }, callback); 57 | }).catch((err) => { 58 | app.call(callback, err); 59 | }); 60 | }); 61 | } 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /web/js/bkjs-user.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * backend.js client 3 | * Vlad Seryakov vseryakov@gmail.com 2018 4 | */ 5 | 6 | (() => { 7 | var app = window.app; 8 | 9 | // True if current credentials are good 10 | app.loggedIn = false; 11 | 12 | // HTTP headers to be sent with every request 13 | app.headers = {}; 14 | 15 | // Current user record 16 | app.user = {}; 17 | 18 | // Secret policy for plain text passwords 19 | app.passwordPolicy = { 20 | '[a-z]+': 'requires at least one lower case letter', 21 | '[A-Z]+': 'requires at least one upper case letter', 22 | '[0-9]+': 'requires at least one digit', 23 | '.{9,}': 'requires at least 9 characters', 24 | }; 25 | 26 | // Verify user secret against the policy 27 | app.checkPassword = function(secret, policy, options) 28 | { 29 | secret = secret || ""; 30 | policy = policy || app.passwordPolicy; 31 | for (var p in policy) { 32 | if (!secret.match(p)) { 33 | return { 34 | status: 400, 35 | message: app.__(policy[p]), 36 | policy: Object.keys(policy).map((x) => (app.__(policy[x]))), 37 | }; 38 | } 39 | } 40 | return ""; 41 | } 42 | 43 | // Try to authenticate with the supplied login and secret 44 | app.login = function(options, callback) 45 | { 46 | if (typeof options == "function") callback = options, options = null; 47 | app.send({ url: options?.url || "/auth", data: options?.data }, (data) => { 48 | app.loggedIn = true; 49 | Object.assign(app.user, data); 50 | app.call(callback); 51 | app.emit("login", options?.path); 52 | }, (err) => { 53 | app.loggedIn = false; 54 | for (const p in app.user) delete app.user[p]; 55 | app.call(callback, err); 56 | app.emit("nologin", err); 57 | }); 58 | } 59 | 60 | // Logout and clear all cookies and local credentials 61 | app.logout = function(options, callback) 62 | { 63 | if (typeof options == "function") callback = options, options = null; 64 | for (const p in app.user) delete app.user[p]; 65 | app.loggedIn = false; 66 | app.sendRequest({ url: options?.url || "/logout" }, (err) => { 67 | app.call(callback, err); 68 | app.emit("logout", err); 69 | }); 70 | } 71 | 72 | // Retrieve current user record, call the callback with the object or error 73 | app.getUser = function(query, callback) 74 | { 75 | if (typeof query == "function") callback = query, query = null; 76 | app.sendRequest({ url: "/auth", data: query }, (err, data) => { 77 | if (!err) Object.assign(app.user, data); 78 | app.call(callback, err, data); 79 | }); 80 | } 81 | 82 | })(); 83 | -------------------------------------------------------------------------------- /web/js/index.html: -------------------------------------------------------------------------------- 1 | nothing here 2 | 3 | -------------------------------------------------------------------------------- /web/js/webpush.js: -------------------------------------------------------------------------------- 1 | // Handle Push events to display messages 2 | console.log("ServiceWorker loaded: Push Notifications"); 3 | self.addEventListener("push", (e) => { 4 | const options = {}; 5 | const data = e.data && e.data.json() || {}; 6 | for (const p of ["actions", "badge", "body" ,"data", "dir", "icon", "image", "lang", "renotify", "requireInteraction", "silent", "tag", "timestamp", "vibrate"]) { 7 | if (typeof data[p] != "undefined") options[p] = data[p]; 8 | } 9 | if (data.body && !data.title) delete options.body; 10 | self.registration.showNotification(data.title || data.body || "New message!", options); 11 | }); 12 | -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /web/webfonts/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/6ce733387b33c2de59b1ea37b1ec815893362e00/web/webfonts/index.html --------------------------------------------------------------------------------