├── .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 |
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(`` + app.templates[template] + "", 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
--------------------------------------------------------------------------------