Files
vnctalk-avatar/app/app.js
T
macanhhuy 0669f87134
Build + push avatar image / build-and-push (push) Failing after 19s
fix: serve avatar files from node process (express.static)
Single-container avatar service: serve the resized files from outputDir
so the frontend can GET /<md5(jid)>.jpg without a separate nginx.
2026-08-22 12:19:50 +07:00

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);
});