Commit 84dc192e authored by Distopico's avatar Distopico
Browse files

Add helper for logs and mailer

parent e64033d6
......@@ -52,4 +52,4 @@ compa.json
bin/lib
server/
build/
src/*
!src/*
......@@ -18,7 +18,7 @@
"adapter": "disk",
"dir": "/tmp/compa/"
},
"webClient": {
"webapp": {
"enable": true,
"debug": false
}
......
{
"name": "compa",
"preferGlobal": "true",
"homepage": "http://joincompa.org/",
"version": "1.0.0-alpha.4",
"license": "AGPL-3.0",
"author": "Distopico <distopico@riseup.net>",
"description": "Compa is a free (as freedom) world wide social directory decentralized and federated with an ActivityPub API.",
"preferGlobal": "true",
"repository": {
"type": "git",
"url": "https://framagit.org/compa/compa.git"
},
"bugs": "https://framagit.org/compa/compa/issues",
"engines": {
"node": ">=4 <=8",
"npm": ">=2"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
......@@ -12,7 +22,6 @@
"dependency-check": "^2.8.0",
"eslint": "^4.10.0",
"eslint-plugin-node": "^5.2.1",
"install": "^0.10.1",
"istanbul-combine": "^0.3.0",
"jsonlint": "^1.6.2",
"nyc": "^11.3.0",
......@@ -25,18 +34,27 @@
"bluebird": "^3.5.1",
"body-parser": "^1.15.2",
"bunyan": "^1.8.12",
"connect-waterline": "^0.1.0",
"cookie-parser": "^1.4.3",
"express": "^4.16.0",
"express-session": "^1.14.1",
"file-stream-rotator": "^0.2.0",
"gm": "^1.22.0",
"helmet": "^3.3.0",
"html-to-text": "^3.3.0",
"juice": "^4.2.2",
"lodash": "^4.17.4",
"method-override": "^2.3.6",
"nodemailer": "^2.7.2",
"serve-favicon": "^2.3.0",
"showdown": "^1.4.1",
"ssl-config": "^1.0.0",
"ua-parser-js": "^0.7.10",
"uuid": "^3.0.0",
"validator": "^9.0.0",
"vue": "^2.5.3",
"vue-server-renderer": "^2.5.3",
"waterline": "^0.13.1-8",
"yargs": "^9.0.1"
},
"bin": {
......@@ -63,11 +81,6 @@
"lint:json": "jsonlint -q package.json compa.json.sample",
"prepublish": "npm run build"
},
"repository": {
"type": "git",
"url": "https://framagit.org/compa/compa.git"
},
"bugs": "https://framagit.org/compa/compa/issues",
"keywords": [
"activitystreams",
"activitypub",
......@@ -81,11 +94,6 @@
"app",
"server"
],
"license": "AGPL-3.0",
"engines": {
"node": ">=4 <=8",
"npm": ">=2"
},
"greenkeeper": {
"ignore": []
}
......
// Print in console utility for compa CLI
//
// Compa -- worldwide social directory decentralized and federated
// Copyright (C) 2017 Distopico <distopico@riseup.net>
// print.js is part of Compa.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
"use strict";
const _ = require("lodash");
const console = require("better-console");
/**
* Console/print utility, inherits the same "better-console" method.
* @module cli/print
*/
const print = _.clone(console);
/**
* Add additionally behavior for `better-console` in error levels,
* when `NODE_ENV` if different to 'development' or 'test' only show the error message,
* in debug mode (development|test) shows stack error.
* @private
* @param {string} method - console method for send
* @param {object[]} args - called arguments
*/
const logError = (method, args) => {
const isDebug = process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development";
let stack = null;
if (!_.isArray(args)) {
console[method].apply(this, args);
return;
}
for (let i = 0; i < args.length; i++) {
const value = args[i];
// Show only error message in production
if (!isDebug && (value instanceof Error)) {
args[i] = value.message;
continue;
}
// Show error stack only in debug mode
if (isDebug && _.has(value, "stack")) {
stack = value.stack;
}
}
console[method].apply(this, args);
if (stack) {
console.log(stack);
}
};
/**
* Extend `error` level with custom behavior in "debug" mode
* @param {...(string|object|number)} args - called arguments
*/
print.error = (...args) => {
logError("error", args);
};
/**
* Extend `warn` level with custom behavior in "debug" mode
* @param {...(string|object|number)} args called arguments
*/
print.warn = (...args) => {
logError("warn", args);
};
module.exports = print;
......@@ -18,3 +18,95 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
"use strict";
const fs = require("fs");
const http = require("http");
const https = require("https");
const express = require("express");
const Promise = require("bluebird");
const sslConfig = require("ssl-config");
const { logger, mailer } = require("./helpers");
/**
* Compa server with support http/https.
*/
class CompaServer {
/**
* Initialize express instance and set server configuration.
* @param {object} config - compa configuration
*/
constructor(config) {
this.config = config;
this.app = express();
}
/**
* Create and setup all server requirements.
* @returns {Promise} on success return express/server instance
*/
create() {
const confServer = this.config.server;
const port = confServer.port;
const hostname = confServer.hostname;
const address = confServer.address || hostname;
const isHTTPS = confServer.key;
return new Promise((resolve, reject) => {
if (process.getuid) {
if (port < 1024 && process.getuid() !== 0) {
return reject(new Error(
"Can't listen ports lower than 1024 on POSIX systems unless you're root"
));
}
}
resolve();
}).then(() => {
return logger.setup(this.config);
}).then((log) => {
this.log = log;
return mailer.setup(this.config, log);
}).then(() => {
if (isHTTPS) {
const readFile = Promise.promisify(fs.readFile);
const key = readFile(confServer.key);
const cert = readFile(confServer.cert);
return [key, cert];
}
return [];
}).spread((key, cert) => {
if (key && cert) {
const ssl = sslConfig("intermediate");
// TODO: bounce?
return https.createServer({
key: key,
cert: cert,
ciphers: ssl.ciphers,
honorCipherOrder: true,
secureOptions: ssl.minimumTLSVersion
}, this.app);
}
return http.createServer(this.app);
}).then((appServer) => {
return Promise.fromCallback((callback) => {
appServer.listen(port, address, () => {
this.log.info("Listening Compa on %s:%s", address, port);
callback(null, this.app);
});
});
});
}
}
module.exports = CompaServer;
// Default configuration for Compa
//
// Compa -- worldwide social directory decentralized and federated
// Copyright (C) 2017 Distopico <distopico@riseup.net>
// defaults.js is part of Compa.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
"use strict";
const path = require("path");
// All available configuration properties, for some properties the default value
// will be defined when the application starts, this is only for reference.
const defaults = {
instance: {
name: "Compa",
maintainer: null,
maintainerUrl: null,
contactEmail: null
},
server: {
port: 1204,
hostname: "127.0.0.1",
address: null,
secret: "keep it in secret",
key: null,
cert: null,
logger: true,
loggerLevel: "info",
loggerType: "stream",
loggerDir: null,
alerts: false,
dataDir: "/var/local/compa",
enableUploads: true,
serverUser: null,
serverGroup: null
},
database: {
adapter: "memory"
},
mailer: {
host: null,
port: 25,
user: null,
pass: null,
useSSL: false,
useTLS: true,
timeout: 30000,
hostname: null,
verbose: false
},
webapp: {
enable: true,
debug: false,
favicon: path.resolve(__dirname, "../public/images/favicon.ico")
}
};
module.exports = defaults;
// Send email logs
//
// Compa -- worldwide social directory decentralized and federated
// Copyright (C) 2017 Distopico <distopico@riseup.net>
// emailStream.js is part of Compa.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
"use strict";
const util = require("util");
const stream = require("stream");
const _ = require("lodash");
const bunyan = require("bunyan");
const mailer = require("./mailer");
const Stream = stream.Writable || stream.Stream;
const transport = new WeakMap();
/**
* Stream to send emails with logs
* @module helpers/emailstream
*/
class EmailStream {
/**
* Create stream, define options and set mailer transport
* @constructor
* @param {object} config - an locals and configuration for send emails
* @param {(string|string[])} config.to - the emails/emails to send message with logs
* @param {string} [config.instanceName=log.hostname] - the Compa instance name for subject
* @param {buffer} [config.ringBuffer] - ring buffer instance with last records
* @param {string} [config.address] - an IP or domain to identify the logs, useful when run
* multiples instances over same machine
*/
constructor(config) {
Stream.call(this);
this.config = _.extend({
to: [],
instanceName: null,
ringBuffer: null,
address: null
}, config);
if (_.isString(config.to)) {
this.config.to = [config.to];
}
this.writable = true;
transport.set(this, mailer.transport);
}
/**
* Send a email when write a log according the level setup,
* also send last records in `ringBuffer`
* @param {object} log - an object data with information as level/hostname/msg etc
*/
write(log) {
const config = this.config;
if (config.to && config.to.length && log.component !== "mail") {
const levelName = bunyan.nameFromLevel[log.level] || `LEVEL_${log.level}`;
// Send email
mailer.send("emailog", {
to: config.to,
subject: util.format(
"[%s] Exception in Compa: %s From %s in %s(%s)",
levelName,
_.truncate((_.get(log, "err.message") || log.msg), 30),
(config.instanceName || log.hostname),
log.hostname,
config.address
),
error: log.err || log,
records: (config.ringBuffer && config.ringBuffer.records || {}),
enviroment: (process.env.NODE_ENV || "NO_ENV").toUpperCase()
});
}
}
/**
* Close transport when stream ends
*/
end() {
const _transport = transport.get(this);
if (_transport) {
_transport.close();
}
}
}
util.inherits(EmailStream, Stream);
module.exports = EmailStream;
// Helper module for Compa
//
// Compa -- worldwide social directory decentralized and federated
// Copyright (C) 2017 Distopico <distopico@riseup.net>
// index.js is part of Compa.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
"use strict";
const defaults = require("./defaults");
const logger = require("./logger");
const mailer = require("./mailer");
/**
* All available utilities for helpers module
* @module helpers
*/
const helpers = {
defaults: defaults,
logger: logger,
mailer: mailer
};
module.exports = helpers;
// Logger utility for compa
//
// Compa -- worldwide social directory decentralized and federated
// Copyright (C) 2017 Distopico <distopico@riseup.net>
// logger.js is part of Compa.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
"use strict";
const fs = require("fs");
const _ = require("lodash");
const path = require("path");
const Promise = require("bluebird");
const cluster = require("cluster");
const uuid = require("uuid");
const bunyan = require("bunyan");
const fileStreamRotator = require("file-stream-rotator");
const emailStream = require("./emailstream");
// Private methods
const getLoggerStreams = Symbol("getLoggerStreams");
/**
* Logger utility class.
* @module helpers/logger
*/
class Logger {
/**
* Initialize the logger configuration.
* @constructor
*/
constructor() {
this.log = null;
this.weblog = null;
this.socketlog = null;
this.ringBuffer = {};
this.logConfig = {
name: "compa",
worker: cluster.worker && cluster.worker.id || 0,
serializers: {
req: bunyan.stdSerializers.req,
res: bunyan.stdSerializers.res,
err: (err) => {
const obj = bunyan.stdSerializers.err(err);
// Remove private properties (with underscore)
return _.pick(obj, _.filter(_.keys(obj), (key) => {
return key[0] !== "_";
}));
},
data: (data) => {
if (data) {
// Remove private data
return _.omit(data, ["password"]);
}
},
user: (user) => {
if (user) {
return {id: user.id, type: user.type};
}
return {id: "<none>"};
},
customer: (customer) => {
if (customer) {
return {token: customer.token, name: customer.name || "<none>"};
}
return {token: "<none>", name: "<none>"};
}
}
};
}
/**
* Build logger stream configuration by type, for 'file' and 'rotate'
* additionally save a log for error levels.
* @private
* @param {string} type - Supported types are 'file', 'rotate' and 'stream'
* if it's not specified or is false the stream will disabled
* @param {object} options - the parameters for define a stream
* @param {buffer} [options.ringBuffer] - stream Buffer for email stream notifications
* @param {string} [options.logName] - name of logger instance, default `server`
* @param {string} [options.name] - compa instance name of alert `emailStream`
* @param {(boolean|object|string[])} [options.alerts] - if `alerts` is `true` and `alertsEmails` is defined
* send message with logs to that email, if a object need `alerts.to` property with email string or array,
* the log level will be send is define with `alerts.level` or default "warn"
* @param {object} options.config - server configuration with logger options, see {@link Logger#setup}
* @returns {object} logger streams configuration
*/
[getLoggerStreams](type, options = {}) {
const errorLevel = {
debug: "warn",
info: "warn",
warn: "error",
error: "fatal",
fatal: "fatal"
};
const config = options.config || {};
const ringBuffer = options.ringBuffer;
const logName = options.logName || "server";