Build + push avatar image / build-and-push (push) Failing after 19s
Single-container avatar service: serve the resized files from outputDir so the frontend can GET /<md5(jid)>.jpg without a separate nginx.
225 lines
6.7 KiB
JavaScript
225 lines
6.7 KiB
JavaScript
var logger = require('morgan');
|
|
var moment = require('moment');
|
|
var express = require('express');
|
|
require('express-async-errors');
|
|
var bodyParser = require('body-parser');
|
|
var multer = require('multer');
|
|
var storage = multer.memoryStorage();
|
|
var upload = multer({ storage: storage });
|
|
var app = express();
|
|
var fs = require('fs');
|
|
var md5 = require('md5');
|
|
var gm = require('gm');
|
|
var jwt = require('jsonwebtoken');
|
|
var env = process.env.NODE_ENV || 'development';
|
|
var cors = require('cors');
|
|
var config = require('../config/vnc-avatarservice.js')[env];
|
|
console.log(moment().format("LTS") + ' Using configuration', config);
|
|
|
|
const corsOpts = {
|
|
origin: true,
|
|
methods: ["POST", "GET", "PUT", "DELETE", "OPTIONS"],
|
|
credentials: true
|
|
};
|
|
|
|
var servicePort = config.servicePort;
|
|
var outputDir = "";
|
|
if (config.outputDir.endsWith("/")) {
|
|
outputDir = config.outputDir;
|
|
} else {
|
|
outputDir = config.outputDir + "/";
|
|
}
|
|
|
|
|
|
var defaultresolution = config.defaultresolution;
|
|
var resolutions = config.resolutions;
|
|
|
|
if (resolutions.indexOf(defaultresolution) == -1) {
|
|
resolutions.push(defaultresolution);
|
|
}
|
|
|
|
// app.use(bodyParser.json()); // for parsing application/json
|
|
app.use(bodyParser.json( { type: 'application/json' }));
|
|
app.use(bodyParser.raw( { limit: '10mb', type: 'image/*' }));
|
|
app.use(logger('dev'));
|
|
app.use(cors(corsOpts));
|
|
|
|
// Serve the resized avatar files (GET /<md5(jid)>.jpg) straight from outputDir.
|
|
// The frontend reads `${avatarServiceUrl}/${hash}.jpg`; historically an nginx
|
|
// sharing the outputDir volume did this, but serving it from the node process
|
|
// keeps the deployment a single container with one port.
|
|
app.use(express.static(outputDir));
|
|
|
|
// function store
|
|
function resizeAndSave(buf, jidhash) {
|
|
if (resolutions.length > 0) {
|
|
for (var i = 0; i < resolutions.length; i++) {
|
|
var reso = resolutions[i];
|
|
var outFileName = outputDir + jidhash + '.jpg';
|
|
if (reso != defaultresolution) {
|
|
outFileName = outputDir + jidhash + '-' + parseInt(reso) + '.jpg';
|
|
}
|
|
console.log("using reso: ", reso);
|
|
gm(buf, 'image.png').quality(90).resize(reso, reso, '!').write(outFileName, function (err){
|
|
if (err) {
|
|
console.log("using reso: ", reso);
|
|
console.log("err: ", err);
|
|
} else {
|
|
console.log("wrote to " + outFileName);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function isDomainAllowed(test, domain) {
|
|
var allowed = false;
|
|
try {
|
|
if (test.indexOf("*.") == 0) {
|
|
var testDom = test.split("*.")[1];
|
|
if (testDom == domain) {
|
|
allowed = true;
|
|
}
|
|
} else {
|
|
if (test == domain) {
|
|
allowed = true;
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
console.log("caught ex: ", ex);
|
|
}
|
|
return allowed;
|
|
}
|
|
|
|
function isAuthenticated(req) {
|
|
console.log("got req: ", req);
|
|
if (req.headers && req.headers.authorization) {
|
|
// console.log("got auth: ", req.headers.authorization);
|
|
if (req.headers.authorization == "Basic YXZhdGFyOmphOGNhZmVpOHdhaXBoN0lldmFoR2hhaG5vaDI=") {
|
|
// old global credentials
|
|
return true;
|
|
} else {
|
|
if (req.params && req.params.jid) {
|
|
var jid = req.params.jid;
|
|
if (jid.indexOf("@") == -1) {
|
|
return false;
|
|
} else {
|
|
var domain = jid.split("@")[1];
|
|
var isAllowedForDomain = false;
|
|
try {
|
|
var token = req.headers.authorization.split("Basic ")[1];
|
|
var jtoken = "";
|
|
var de64token = new Buffer.from(token, 'base64').toString('ascii');
|
|
if (de64token.indexOf(":") > -1) {
|
|
jtoken = de64token.split(":")[1];
|
|
} else {
|
|
jtoken = de64token;
|
|
}
|
|
var authInfo = jwt.verify(jtoken, config.jwtsecret);
|
|
if (typeof(authInfo.vncdomain) === 'object') {
|
|
for (var j = 0; j < authInfo.vncdomain.length; j++) {
|
|
if (isDomainAllowed(authInfo.vncdomain[j], domain)) {
|
|
isAllowedForDomain = true;
|
|
}
|
|
}
|
|
} else {
|
|
isAllowedForDomain = isDomainAllowed(authInfo.vncdomain, domain);
|
|
}
|
|
} catch (ex) {
|
|
console.log("error parsing token: ", ex);
|
|
}
|
|
return isAllowedForDomain;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
app.put('/avatarupload/:jid', async function (req, res) {
|
|
var jid = req.params.jid;
|
|
|
|
// console.log("req: ", req);
|
|
if (jid.indexOf("@") == -1) {
|
|
res.status(500).json('invalid jid/mail');
|
|
} else {
|
|
var jidhash = md5(jid);
|
|
var uploadedImageBuffer = Buffer.from(req.body);
|
|
if (isAuthenticated(req)) {
|
|
resizeAndSave(uploadedImageBuffer, jidhash);
|
|
res.json(jidhash);
|
|
} else {
|
|
res.status(401).json('invalid auth');
|
|
}
|
|
|
|
|
|
}
|
|
});
|
|
|
|
app.delete('/avatarupload/:jid', async function (req, res) {
|
|
var jid = req.params.jid;
|
|
if (jid.indexOf("@") == -1) {
|
|
res.status(500).json('invalid jid/mail');
|
|
} else {
|
|
var jidhash = md5(jid);
|
|
if (isAuthenticated(req)) {
|
|
try {
|
|
var files = fs.readdirSync(outputDir).filter(fn => fn.startsWith(jidhash));
|
|
if (files.length > 0) {
|
|
console.log("files: ", files);
|
|
for (var i = 0; i < files.length; i++) {
|
|
fs.unlink(outputDir + "/" + files[i], (err) => {
|
|
if (err) {
|
|
console.log("error removing file: ", err);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
console.log("error parsing token: ", ex);
|
|
}
|
|
res.status(200).json("ok");
|
|
} else {
|
|
res.status(401).json(null);
|
|
}
|
|
}
|
|
});
|
|
|
|
app.options('/avatarupload/info', cors(corsOpts));
|
|
app.post('/avatarupload/info', cors(corsOpts), async function (req, res) {
|
|
var ids = req.body.ids;
|
|
if (!!ids && typeof ids == "object" && Array.isArray(ids)) {
|
|
var result = {};
|
|
try {
|
|
for (var i = 0; i < ids.length; i++) {
|
|
if (ids[i].length == 32) {
|
|
const fn = outputDir + "/" + ids[i] + ".jpg";
|
|
const fdata = fs.statSync(fn, { throwIfNoEntry: false });
|
|
result[ids[i]] = (!!fdata && !!fdata.ctime) ? fdata.ctime.valueOf() : -1;
|
|
} else {
|
|
result[ids[i]] = -1;
|
|
}
|
|
}
|
|
res.status(200).json(result);
|
|
} catch (e) {
|
|
console.log("avatarinfo error: ", e);
|
|
res.status(500).json(null);
|
|
}
|
|
} else {
|
|
res.status(500).json(null);
|
|
}
|
|
});
|
|
|
|
|
|
app.get('/avatarupload/health', async function (req, res) {
|
|
console.log(moment().format("LTS") + ' health check called');
|
|
res.status(200).json({ status: 'OK'});
|
|
});
|
|
|
|
var server = app.listen(servicePort, function () {
|
|
var host = server.address().address;
|
|
var port = server.address().port;
|
|
console.log(moment().format("LTS") + ' Service endpoint is http://%s:%s/avatarupload', host, port);
|
|
});
|