initial commit

This commit is contained in:
JJ Kasper
2018-05-17 14:31:05 -05:00
commit 2c90d2e7dd
79 changed files with 10684 additions and 0 deletions

34
src/app.hooks.js Normal file
View File

@@ -0,0 +1,34 @@
// Application hooks that run for every service
const logger = require('./hooks/logger');
module.exports = {
before: {
all: [ logger() ],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
after: {
all: [ logger() ],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [ logger() ],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};

117
src/app.js Normal file
View File

@@ -0,0 +1,117 @@
const favicon = require('serve-favicon');
const fs = require('fs');
const path = require('path');
const compress = require('compression');
const cors = require('cors');
const helmet = require('helmet');
const cookieParser = require('cookie-parser')();
const RateLimit = require('express-rate-limit');
const trustIPs = require('./trustIPs');
const logger = require('winston');
const feathers = require('@feathersjs/feathers');
const express = require('@feathersjs/express');
const configuration = require('@feathersjs/configuration');
const hostConfig = require('../config/host.json');
const middleware = require('./middleware');
const services = require('./services');
const appHooks = require('./app.hooks');
const channels = require('./channels');
const authentication = require('./authentication');
const dev = process.env.NODE_ENV !== 'production';
const basePath = require('../util/basePath');
const stripBase = require('../util/stripBase');
const getUrl = require('../util/getUrl');
const { parse } = require('url');
const nxt = require('next')({ dev, quiet: true });
const nxtHandler = nxt.getRequestHandler();
const app = express(feathers());
app.startNext = () => nxt.prepare();
app.configure(configuration()); // Load app configuration
Object.keys(hostConfig).forEach(key => ( // load host config
app.set(key, hostConfig[key])
));
app.set('didSetup', false);
try {
fs.statSync(path.join(__dirname, '..', 'db', '.didSetup'));
app.set('didSetup', true);
}
catch(err) {
app.use((req, res, next) => {
req.doSetup = !app.get('didSetup');
next();
});
}
const authLimit = new RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5, // 5 attempts then block
delayAfter: 3, // slow down after 3 fails
delayMs: 2 * 1000
});
app.authLimit = authLimit;
app.use(getUrl('auth'), authLimit);
app.patch(getUrl('users/*'), authLimit);
// Enable CORS, security, compression, favicon and body parsing
trustIPs(app);
app.use(cors());
app.use(helmet({
hidePoweredBy: { setTo: 'hamsters' }
}));
if(!dev) app.use(compress());
app.use(express.json()); // use { limit } option to increase max post size
app.use(express.urlencoded({ extended: true }));
app.use(getUrl('/'), favicon('favicon.ico'));
app.configure(express.rest()); // Set up Plugins and providers
app.configure(middleware); // middleware/index.js
app.configure(authentication); // Set up authentication
app.configure(services); // Set up our services (see `services/index.js`)
app.configure(channels); // Set up event channels (see channels.js)
const checkJWT = async (req, res, next) => {
const result = await req.app.authenticate('jwt', {})(req);
if(result.success) {
req.jwt = req.cookies.jwt;
delete result.data.user.password;
req.user = result.data.user;
}
next();
};
nxt.setAssetPrefix(basePath); // setup next.js routes
[ '/', '/logout', '/new', '/settings' ]
.forEach(route => {
app.get(getUrl(route), cookieParser, checkJWT, (req, res) => {
const { query } = parse(req.url, true);
nxt.render(req, res, route, query);
});
});
[ '/k', '/edit' ]
.forEach(route => {
app.get(getUrl(route + '/:id'), cookieParser, checkJWT, (req, res) => {
nxt.render(req, res, route, { id: req.params.id });
});
});
const notFound = express.notFound();
app.use((req, res, next) => {
let accept = req.get('accept');
if(accept && accept.toLowerCase() === 'application/json')
return notFound(req, res, next);
if(req.url.substr(0, basePath.length) !== basePath)
return nxt.render404(req, res);
nxtHandler(req, res, parse(stripBase(req.url), true));
});
app.use(express.errorHandler({ logger }));
app.hooks(appHooks);
module.exports = app;

42
src/authentication.js Normal file
View File

@@ -0,0 +1,42 @@
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const local = require('@feathersjs/authentication-local');
const getUrl = require('../util/getUrl');
module.exports = function (app) {
const config = app.get('authentication');
config.path = getUrl(config.path);
config.service = getUrl('users');
// Set up authentication with the secret
app.configure(authentication(
Object.assign({}, config, {
cookie: {
enabled: true,
httpOnly: false,
secure: false,
name: 'jwt',
}
})
));
app.configure(jwt());
app.configure(local());
// The `authentication` service is used to create a JWT.
// The before `create` hook registers strategies that can be used
// to create a new valid JWT (e.g. local or oauth2)
app.service(config.path).hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies),
ctx => {
ctx.app.authLimit.resetKey(ctx.params.ip);
return ctx;
}
],
remove: [
authentication.hooks.authenticate('jwt')
]
}
});
};

61
src/channels.js Normal file
View File

@@ -0,0 +1,61 @@
module.exports = function(app) {
if(typeof app.channel !== 'function') {
// If no real-time functionality has been configured just return
return;
}
app.on('connection', connection => {
// On a new real-time connection, add it to the anonymous channel
app.channel('anonymous').join(connection);
});
app.on('login', (authResult, { connection }) => {
// connection can be undefined if there is no
// real-time connection, e.g. when logging in via REST
if(connection) {
// Obtain the logged in user from the connection
// const user = connection.user;
// The connection is no longer anonymous, remove it
app.channel('anonymous').leave(connection);
// Add it to the authenticated user channel
app.channel('authenticated').join(connection);
// Channels can be named anything and joined on any condition
// E.g. to send real-time events only to admins use
// if(user.isAdmin) { app.channel('admins').join(connection); }
// If the user has joined e.g. chat rooms
// if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel));
// Easily organize users by email and userid for things like messaging
// app.channel(`emails/${user.email}`).join(channel);
// app.channel(`userIds/$(user.id}`).join(channel);
}
});
// eslint-disable-next-line no-unused-vars
app.publish((data, hook) => {
// Here you can add event publishers to channels set up in `channels.js`
// To publish only for a specific event use `app.publish(eventname, () => {})`
console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line
// e.g. to publish all service events to all authenticated users use
return app.channel('authenticated');
});
// Here you can also add service specific event publishers
// e..g the publish the `users` service `created` event to the `admins` channel
// app.service('users').publish('created', () => app.channel('admins'));
// With the userid and email organization from above you can easily select involved users
// app.service('messages').publish(() => {
// return [
// app.channel(`userIds/${data.createdBy}`),
// app.channel(`emails/${data.recipientEmail}`)
// ];
// });
};

23
src/hooks/logger.js Normal file
View File

@@ -0,0 +1,23 @@
// A hook that logs service method before, after and error
// See https://github.com/winstonjs/winston for documentation
// about the logger.
const logger = require('winston');
// To see more detailed messages, uncomment the following line
// logger.level = 'debug';
module.exports = function () {
return context => {
// This debugs the service call and a stringified version of the hook context
// You can customize the mssage (and logger) to your needs
logger.debug(`${context.type} app.service('${context.path}').${context.method}()`);
if(typeof context.toJSON === 'function') {
logger.debug('Hook Context', JSON.stringify(context, null, ' '));
}
if (context.error) {
logger.error(context.error);
}
};
};

14
src/index.js Normal file
View File

@@ -0,0 +1,14 @@
const logger = require('winston');
const app = require('./app');
const port = app.get('port');
const server = app.listen(port);
app.startNext();
server.on('listening', () =>
logger.info('MYKB listening at http://%s:%d', app.get('host'), port)
);
process.on('unhandledRejection', (reason, p) =>
logger.error('Unhandled Rejection at: Promise ', p, reason)
);

11
src/middleware/index.js Normal file
View File

@@ -0,0 +1,11 @@
// eslint-disable-next-line no-unused-vars
module.exports = function (app) {
// Add your custom middleware here. Remember, that
// in Express the order matters
// add req.ip to feathers
app.use((req, res, next) => {
req.feathers.ip = req.ip;
next();
});
};

14
src/models/users.model.js Normal file
View File

@@ -0,0 +1,14 @@
const NeDB = require('nedb');
const path = require('path');
module.exports = function (app) {
const dbPath = app.get('nedb');
const Model = new NeDB({
filename: path.join(dbPath, 'users.db'),
autoload: true
});
Model.ensureIndex({ fieldName: 'email', unique: true });
return Model;
};

View File

@@ -0,0 +1,225 @@
const errors = require('@feathersjs/errors');
const gitP = require('simple-git/promise');
const fs = require('fs-extra');
const path = require('path');
const loadDocs = require('./loadDocs');
const comparableFields = [ 'name', 'dir' ];
let git;
class Service {
constructor (options) {
this.options = options || {};
this.docsDir = this.options.docsDir;
this.$limit = this.options.paginate.default;
this.useGit = this.options.useGit;
this.maxDocSize = 1024 * 100; // 100 kb (don't try sending if bigger)
this.docs = {};
this.updTimeouts = {};
loadDocs.bind(this)();
if(this.useGit) {
git = gitP(this.docsDir);
fs.stat(path.join(this.docsDir, '.git'), async err => {
if(err && err.code === 'ENOENT') {
git.init().then(() => {
if(this.numInitDocs === 0) return;
git.add('./*').then(() => git.commit('initial commit'));
});
}
});
}
}
async getMd (id) {
const doc = this.docs[id];
if(doc.md) return doc.md;
const docPath = path.join(this.docsDir, doc.dir, doc.name);
const { size } = await fs.stat(docPath);
if(size > this.maxDocSize) return 'Document is too big to display...';
const buff = await fs.readFile(docPath);
return buff.toString();
}
async isMatch (id, $search) {
const doc = this.docs[id];
const name = doc.name.toLowerCase();
if(name.indexOf($search) > -1) return true;
const dir = doc.dir.toLowerCase();
if(dir.indexOf($search) > -1) return true;
const relPath = dir + (dir.length > 0 ? '/' : '') + name;
if(relPath.toLowerCase().indexOf($search) > -1) return true;
let md = await this.getMd(id);
md = md.toLowerCase();
if(md.indexOf($search) > -1) return true;
return false;
}
async checkRmDir (dir) {
if(dir === '') return;
const parts = dir.split('/');
const n = parts.length;
for(let i = 0; i < n; i++) {
const dir = parts.filter((p, pIdx) => pIdx < (n - i)).join('/');
const docsInDir = await this.find({ query: { dir } });
if(docsInDir.total === 0) {
try {
await fs.rmdir(path.join(this.docsDir, dir));
} catch(err) { return; }
} else return;
}
}
async find (params) {
const { query } = params;
let { $limit, $skip, $search, $select, $sort } = query;
if($skip < 0) $skip = 0;
$limit = $limit || this.$limit;
if($search) {
$search = $search.toLowerCase().trim();
}
let ids = Object.keys(this.docs);
const data = [];
const toComp = comparableFields.filter(f => typeof query[f] !== 'undefined');
if(toComp.length > 0) {
ids = ids.filter(id => {
const doc = this.docs[id];
return !toComp.some(f => doc[f] !== query[f]);
});
}
if($search) {
ids = await Promise.all(
ids.map(async id => await this.isMatch(id, $search) ? id : null)
);
ids = ids.filter(Boolean);
}
if(ids.length === 0) return { total: 0, data };
if($sort) {
const sortKey = Object.keys($sort).pop();
const ascDesc = $sort[sortKey];
if(this.docs[ids[0]][sortKey] || sortKey === 'dirName') { // make sure valid key
ids.sort((a, b) => {
let val1, val2, parseVals;
const docA = this.docs[a],
docB = this.docs[b];
if(sortKey === 'dirName') {
parseVals = doc => path.join(doc.dir, doc.name);
}
else {
parseVals = doc => doc[sortKey];
}
[val1, val2] = [docA, docB].map(parseVals);
let c = 0;
if(val1 < val2) c = -1;
else if(val1 > val2) c = 1;
return c * ascDesc;
});
}
}
for(let i = $skip || 0; i < ids.length && data.length < $limit; i++) {
const id = ids[i];
let doc = $select && !$select.md ? this.docs[id] : this.get(id);
if($select) {
let _doc = {};
$select.forEach(k => _doc[k] = doc[k]);
doc = _doc;
}
data.push(doc);
}
return { total: ids.length, data };
}
async get (id, params) { // eslint-disable-line no-unused-vars
const doc = this.docs[id];
if(!doc) throw new errors.NotFound(`No record found for id '${id}'`);
if(!doc.md) doc.md = await this.getMd(id);
return this.docs[id];
}
async create (data, params) { // eslint-disable-line no-unused-vars
if(Array.isArray(data)) {
return await Promise.all(data.map(current => this.create(current)));
}
const { name, dir, md } = data;
try {
const rPath = path.join(dir, name);
const docPath = path.join(this.docsDir, rPath);
await fs.outputFile(docPath, md);
if(this.useGit) {
git.add(rPath).then(() => git.commit(`added doc ${rPath}`));
}
const ts = new Date().toJSON();
return this.setDoc(path.join(dir, name), ts, md, ts);
}
catch (err) {
throw new errors.GeneralError('could not create doc');
}
}
async update (id, data, params) { // eslint-disable-line no-unused-vars
throw new errors.MethodNotAllowed('can not update on docs service');
}
async patch (id, data, params) { // eslint-disable-line no-unused-vars
const doc = this.docs[id];
if(!doc) throw new errors.NotFound(`No record found for id '${id}'`);
let { name, dir, md } = data;
let diffDir = Boolean(doc.dir !== dir);
if(!name) name = doc.name;
if(typeof dir !== 'string') {
dir = doc.dir;
diffDir = false;
}
const rPath = path.join(dir, name);
const docPath = path.join(this.docsDir, rPath);
if(name !== doc.name || diffDir) {
const oldRPath = path.join(doc.dir, doc.name);
const oldPath = path.join(this.docsDir, oldRPath);
await fs.ensureDir(path.join(this.docsDir, dir));
await fs.move(oldPath, docPath);
if(this.useGit) {
git.rm(oldRPath)
.then(() => git.add(rPath))
.then(() => git.commit(`renamed doc ${oldRPath} ${rPath}`));
}
id = this.getId(path.join(dir, name));
this.docs[id] = Object.assign({}, doc, { id, name, dir });
delete this.docs[doc.id];
if(diffDir) this.checkRmDir(doc.dir);
}
if(md) {
await fs.writeFile(docPath, md);
if(this.useGit) {
git.add(rPath).then(() => git.commit(`updated doc ${rPath}`));
}
this.docs[id].md = md;
}
return this.docs[id];
}
async remove (id, params) { // eslint-disable-line no-unused-vars
const doc = this.docs[id];
if(!id) throw new errors.NotFound(`No record found for id '${id}'`);
const rPath = path.join(doc.dir, doc.name);
const docPath = path.join(this.docsDir, rPath);
await fs.unlink(docPath);
if(this.useGit) {
git.rm(rPath).then(() => git.commit(`removed doc ${rPath}`));
}
delete this.docs[id];
this.checkRmDir(doc.dir);
return doc;
}
}
module.exports = function (options) {
return new Service(options);
};
module.exports.Service = Service;

View File

@@ -0,0 +1,91 @@
const { authenticate } = require('@feathersjs/authentication').hooks;
const { checkDir, checkName } = require('../../../util/checkDirParts');
const { disable, invalid, adminOnly } = require('../hooksUtil');
const getUrl = require('../../../util/getUrl');
const nameIsValid = name => {
name = checkName(name);
if(!name) return invalid('name');
if(name.substr(name.length - 3).toLowerCase() !== '.md') {
name += '.md';
}
return name;
};
const dirIsValid = dir => {
dir = checkDir(dir);
if(!dir && dir !== 0) return invalid('dir');
else if(dir === 0) return '';
return dir;
};
const mdIsValid = md => {
if(typeof md !== 'string' || md.trim().length === 0) {
return invalid('md');
}
return md;
};
const pathTaken = async (name, dir, app) => {
const matches = await app.service(getUrl('docs')).find({ query: { name, dir } });
if(matches.total > 0) {
return invalid(null, 'filename is taken');
}
};
module.exports = {
before: {
all: [ authenticate('jwt') ],
find: [],
get: [],
create: [ async ctx => {
const { app, data } = ctx;
let { name, dir, md } = data;
const k = {};
k.name = nameIsValid(name);
k.dir = dirIsValid(dir);
k.md = mdIsValid(md);
await pathTaken(k.name, k.dir, app);
ctx.data = k;
return ctx;
}],
update: [ disable ],
patch: [ async ctx => {
const { data, app } = ctx;
const { name, dir, md } = data;
const k = {};
if(name) k.name = nameIsValid(name);
if(typeof dir === 'string') k.dir = dirIsValid(dir); // allow empty string
if(name || typeof dir === 'string') {
let checkName, checkDir;
if(!name || typeof dir !== 'string') {
const doc = await app.service(getUrl('docs')).get(ctx.id);
if(!name) checkName = doc.name;
if(typeof dir !== 'string') checkDir = doc.dir;
}
await pathTaken(k.name || checkName,
typeof k.dir === 'string' ? k.dir : checkDir, app);
}
if(md) k.md = mdIsValid(md);
if(Object.keys(k).length === 0) invalid(null, 'nothing to update');
ctx.data = k;
return ctx;
}],
remove: [ adminOnly ]
},
after: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};

View File

@@ -0,0 +1,29 @@
// Initializes the `docs` service on path `/docs`
const createService = require('./docs.class.js');
const hooks = require('./docs.hooks');
const getUrl = require('../../../util/getUrl');
module.exports = function (app) {
const paginate = app.get('paginate');
const docsDir = app.get('docsDir');
const cacheSize = app.get('cacheSize');
const useGit = app.get('useGit');
const options = {
name: 'docs',
paginate,
docsDir,
cacheSize,
useGit
};
const url = getUrl('docs');
// Initialize our service with any options it requires
app.use(url, createService(options));
// Get our initialized service so that we can register hooks and filters
const service = app.service(url);
service.hooks(hooks);
};

View File

@@ -0,0 +1,64 @@
const chokidar = require('chokidar');
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const crypto = require('crypto');
async function loadDocs() {
const { docsDir, cacheSize } = this.options;
this.numInitDocs = glob.sync(path.join(docsDir, '**/*.md')).length;
this.cached = 0;
this.loaded = false;
const getId = relPath => (crypto.createHash('sha1')
.update(relPath).digest().toString('base64')
.substr(0, 16).split('/').join('_')
);
this.getId = getId;
const setDoc = (relPath, created, md, updated) => {
const id = getId(relPath);
let dir = relPath.split('/');
const name = dir.pop();
dir = dir.join('/');
const doc = {
id, name, dir, created, md, updated
};
this.docs[id] = doc;
return doc;
};
this.setDoc = setDoc;
const watcher = chokidar.watch(path.join(docsDir, '/**/*.md'), { persistent: true });
const relPath = path => {
path = path.split(docsDir).pop();
if(path.substr(0, 1) === '/') path = path.substr(1);
return path;
};
const handleDoc = async (path, stats) => {
const rPath = relPath(path);
if(!stats) stats = await fs.stat(path);
else { // stats was set so it's a change event
const changedDoc = this.docs[getId(rPath)];
if(changedDoc && changedDoc.md) this.cached -= changedDoc.md.length;
}
const { birthtime, size, mtime } = stats;
let md = null;
if(size < this.maxDocSize && (this.cached + size) < cacheSize) {
md = await fs.readFile(path);
md = md.toString();
this.cached += md.length;
}
setDoc(rPath, birthtime, md, mtime);
};
watcher.on('add', handleDoc); // file added
watcher.on('change', handleDoc); // file changed (rename triggers unlink then add)
watcher.on('unlink', path => { // file removed
const id = getId(relPath(path));
if(this.docs[id] && this.docs[id].md) {
this.cached -= this.docs[id].md.length;
}
delete this.docs[id];
});
}
module.exports = loadDocs;

23
src/services/hooksUtil.js Normal file
View File

@@ -0,0 +1,23 @@
const { BadRequest, Forbidden } = require('@feathersjs/errors');
const isAdmin = ctx => {
const { params } = ctx;
return Boolean(params.user && params.user.admin);
};
module.exports = {
disable: () => {
throw new BadRequest('method not allowed');
},
invalid: (field, msg) => {
throw new BadRequest(msg || `invalid ${field} value`);
},
isAdmin: isAdmin,
adminOnly: ctx => {
if(!isAdmin(ctx)) throw new Forbidden('invalid permission');
return ctx;
}
};

6
src/services/index.js Normal file
View File

@@ -0,0 +1,6 @@
const users = require('./users/users.service.js');
const docs = require('./docs/docs.service.js');
module.exports = function (app) {
app.configure(users);
app.configure(docs);
};

View File

@@ -0,0 +1,80 @@
const { authenticate } = require('@feathersjs/authentication').hooks;
const { Forbidden } = require('@feathersjs/errors');
const fs = require('fs');
const path = require('path');
const { disable, invalid, isAdmin, adminOnly } = require('../hooksUtil');
const {
hashPassword, protect
} = require('@feathersjs/authentication-local').hooks;
const invalidStr = str => Boolean(
typeof str !== 'string' || str.trim().length === 0);
module.exports = {
before: {
all: [],
find: [ authenticate('jwt') ],
get: [ authenticate('jwt') ],
create: [
async ctx => {
const { data, app } = ctx;
if(app.get('didSetup') && !isAdmin(ctx)) {
throw new Forbidden('invalid permission');
}
const { email, password, admin } = data;
if(invalidStr(email)) invalid('email');
if(invalidStr(password)) invalid('password');
if(typeof admin !== 'boolean') invalid('admin');
ctx.data = { email, password, admin };
return ctx;
},
hashPassword()
],
update: [ disable ],
patch: [ authenticate('local'),
async ctx => {
const { newPassword } = ctx.data;
if(invalidStr(newPassword)) invalid('newPassword');
ctx.data = { password: newPassword };
await hashPassword()(ctx);
ctx.app.authLimit.resetKey(ctx.params.ip);
return ctx;
}
],
remove: [ authenticate('jwt'), adminOnly ]
},
after: {
all: [
// Make sure the password field is never sent to the client
// Always must be the last hook
protect('password')
],
find: [],
get: [],
create: [
async ctx => {
const { app } = ctx;
if(app.get('didSetup')) return ctx;
app.set('didSetup', true);
fs.writeFileSync( // create empty file so we cant stat it
path.join(__dirname, '..', '..', '..', 'db', '.didSetup'), '');
return ctx;
}
],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};

View File

@@ -0,0 +1,25 @@
// Initializes the `users` service on path `/users`
const createService = require('feathers-nedb');
const createModel = require('../../models/users.model');
const hooks = require('./users.hooks');
const getUrl = require('../../../util/getUrl');
module.exports = function (app) {
const Model = createModel(app);
const paginate = app.get('paginate');
const options = {
name: 'users',
Model,
paginate
};
const url = getUrl('users');
// Initialize our service with any options it requires
app.use(url, createService(options));
// Get our initialized service so that we can register hooks and filters
const service = app.service(url);
service.hooks(hooks);
};

50
src/trustIPs.js Normal file
View File

@@ -0,0 +1,50 @@
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const fetch = require('isomorphic-unfetch');
const ips = require('../config/trustIPs.json');
ips.push('loopback');
const cfv4 = 'https://www.cloudflare.com/ips-v4';
const cfv6 = 'https://www.cloudflare.com/ips-v6';
const cfConf = path.join(__dirname, '../config/cfIPs.json');
const refreshInterval = 24 * 60 * 60 * 1000;
const getIps = str => {
str = str.split('\n').map(ip => ip.trim());
str = str.filter(ip => ip.length !== 0);
return str;
};
const getCfIps = async app => {
const cfIps = [];
let res = await fetch(cfv4);
if(res.ok) {
cfIps.push(...getIps(await res.text()));
}
res = await fetch(cfv6);
if(res.ok) {
cfIps.push(...getIps(await res.text()));
}
fs.writeFile(cfConf, JSON.stringify(cfIps, null, 2), err => {
if(err) console.error(err);
});
app.set('trust proxy', [...ips, ...cfIps]);
};
module.exports = app => {
if(!app.get('trustCloudflare')) {
return app.set('trust proxy', ips);
}
fs.readFile(cfConf, async (err, buff) => {
if(err) {
if(err.code === 'ENOENT') getCfIps(app);
else return console.error(err);
}
else {
const cfIps = JSON.parse(buff.toString());
app.set('trust proxy', [...ips, ...cfIps]);
}
setInterval(() => getCfIps(app), refreshInterval);
});
};