diff --git a/config/default.js b/config/default.js index 32e0de2..357e440 100644 --- a/config/default.js +++ b/config/default.js @@ -23,9 +23,6 @@ module.exports = { // File that handles Liquid Galaxy commands (such as flyto). queriesPath: '/tmp/query.txt', - // Run Cron jobs (such as alive reports). - cronJobsEnabled: true, - // Firebase repository where all alive reports will be sent. firebase: { apiKey: 'AIzaSyAddzwazFGRJiC-GW35Zgr7XdhUk8x0890', diff --git a/firebase/database.rules.json b/firebase/database.rules.json index 94786a3..02a2e9e 100644 --- a/firebase/database.rules.json +++ b/firebase/database.rules.json @@ -1,29 +1,76 @@ { "rules": { - "up": { - "$publicIp": { + "servers": { + "$uid": { + ".write": "auth.token.email.beginsWith($uid + '@')", ".read": true, - ".validate": "$publicIp.matches(/^[0-9]{1,3}%[0-9]{1,3}%[0-9]{1,3}%[0-9]{1,3}$/)", - "$reportUid": { - ".write": "!data.exists() && newData.exists()", - ".validate": "newData.hasChildren(['localIp', 'port', 'timestamp'])", - "localIp": { - ".validate": "newData.val().matches(/^(10|172|192)\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$/)" + "displayName": { + ".validate": "newData.val().length < 200" + }, + "lastOnline": { + ".validate": "newData.val() === now" + }, + "isOnline": { + ".validate": "newData.isBoolean()" + }, + "hasPassword": { + ".validate": "newData.isBoolean()" + }, + "$other": { + ".validate": false + } + } + }, + "passwords": { + "$serverUid": { + ".write": "auth.token.email.beginsWith($serverUid + '@') || newData.val() === data.val()", + ".read": false, + ".validate": "newData.isString()" + } + }, + "queue": { + "$serverUidWithKey": { + ".write": true, + ".read": true, + "$queueUid": { + "type": { + ".validate": "newData.isString()" }, - "port": { - ".validate": "newData.val() > 0 && newData.val() < 65535" + "value": { + ".validate": "newData.isString()" }, "timestamp": { ".validate": "newData.val() === now" }, - "optional": { + "$other": { + ".validate": false + } + } + } + }, + "ips": { + "$ip": { + ".read": true, + ".validate": "$ip.matches(/^[0-9]{1,3}:[0-9]{1,3}:[0-9]{1,3}:[0-9]{1,3}$/)", + "$serverUid": { + ".write": "auth.token.email.beginsWith($serverUid + '@') || newData.val() === data.val()", + "displayName": { ".validate": "newData.val().length < 200" }, - "$other": { + "lastOnline": { + ".validate": "newData.val() === now" + }, + "isOnline": { + ".validate": "newData.isBoolean()" + }, + "$other":{ ".validate": false } } } + }, + "$other": { + ".validate": false } } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 422cf8c..157ccf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -536,11 +536,6 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cors": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.3.tgz", - "integrity": "sha1-TPeOHSMymnSWsvwiJbd8pbteuAI=" - }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -1904,6 +1899,11 @@ "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", "dev": true }, + "generate-password": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.3.0.tgz", + "integrity": "sha1-TaTBVFMNIcGZWneqxaPqBIgvyK0=" + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -2663,14 +2663,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=" }, "mocha": { "version": "3.4.2", @@ -3431,6 +3429,11 @@ "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", "dev": true }, + "shortid": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.8.tgz", + "integrity": "sha1-AzsRfWoul1gE9vCWnb59PQs1UTE=" + }, "sinon": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.4.tgz", diff --git a/package.json b/package.json index 09ca3fb..d1a5f50 100644 --- a/package.json +++ b/package.json @@ -76,16 +76,18 @@ "config": "^1.25.1", "connect-redis": "^3.2.0", "cookie-parser": "^1.4.3", - "cors": "^2.8.3", "cron": "^1.2.1", "debug": "^2.6.1", "express": "^4.14.1", "firebase": "^4.1.2", + "generate-password": "^1.3.0", "http": "0.0.0", "ip": "^1.1.5", + "mkdirp": "^0.5.1", "mongoose": "^4.8.4", "morgan": "^1.8.1", "public-ip": "^2.3.5", + "shortid": "^2.2.8", "socket.io": "^2.0.3", "uuid": "^3.1.0", "validator": "^7.0.0" diff --git a/src/cron/CronTask.js b/src/cron/CronTask.js new file mode 100644 index 0000000..fadc3be --- /dev/null +++ b/src/cron/CronTask.js @@ -0,0 +1,25 @@ +const { CronJob } = require('cron'); + +const log = require('../helpers/log'); + +class CronTask { + constructor(name, cronTime, action) { + const newAction = async () => { + log.dev(`[CRON] Started "${name}"`); + await action(); + log.dev(`[CRON] Finished "${name}"`); + }; + this.action = newAction; + this.cronJob = new CronJob(cronTime, newAction); + } + + executeOnce() { + return this.action(); + } + + start() { + this.cronJob.start(); + } +} + +module.exports = CronTask; diff --git a/src/cron/index.js b/src/cron/index.js index 8729a4a..c01a5ee 100644 --- a/src/cron/index.js +++ b/src/cron/index.js @@ -1,28 +1,3 @@ -const { CronJob } = require('cron'); +const CronTask = require('./CronTask'); -const log = require('../helpers/log'); -const { up } = require('../services'); - -class CronTask { - constructor(name, cronTime, action) { - const newAction = async () => { - log.dev(`[CRON] Started "${name}"`); - await action(); - log.dev(`[CRON] Finished "${name}"`); - }; - this.cronJob = new CronJob(cronTime, newAction); - } -} - -const cronTasks = [ - new CronTask('Report Alive', '0,30 * * * * *', () => up.reportAlive()), -]; - -function startAll() { - this.cronTasks.map(cronTask => cronTask.cronJob.start()); -} - -module.exports = { - cronTasks, - startAll, -}; +module.exports = { CronTask }; diff --git a/src/firebase/__tests__/.eslintrc b/src/firebase/__tests__/.eslintrc new file mode 100644 index 0000000..7eeefc3 --- /dev/null +++ b/src/firebase/__tests__/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/src/firebase/__tests__/utils.test.js b/src/firebase/__tests__/utils.test.js new file mode 100644 index 0000000..046a9a1 --- /dev/null +++ b/src/firebase/__tests__/utils.test.js @@ -0,0 +1,10 @@ +const { expect } = require('chai'); +const { encodeUid } = require('../utils'); + +describe('Firebase Auth', () => { + it('encoded email address should decode alright', () => { + const UID = 'loLlipOp'; + const expected = [108, 111, 76, 108, 105, 112, 79, 112].join(''); + expect(encodeUid(UID)).to.equal(expected); + }); +}); diff --git a/src/firebase/auth.js b/src/firebase/auth.js new file mode 100644 index 0000000..b4325fe --- /dev/null +++ b/src/firebase/auth.js @@ -0,0 +1,76 @@ +/** + * Authenticate server. + * Signs up new server with random credentials (stored in data/credentials.txt) + * Sign in with the stored credentials. + * (format deviceUid:editKey:devicePassword) + */ + +const Promise = require('bluebird'); +const fs = require('fs'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const shortid = require('shortid'); +const generatePassword = require('generate-password'); +const firebase = require('firebase'); + +const { encodeUid } = require('./utils'); + +const readFile = Promise.promisify(fs.readFile); +const writeFile = Promise.promisify(fs.writeFile); +const createDir = Promise.promisify(mkdirp); + +const CREDENTIALS_PATH = path.join('data/credentials.txt'); + +async function readCredentials() { + try { + const contents = await readFile(CREDENTIALS_PATH, { encoding: 'utf-8' }); + const [uid, editKey, password] = contents.split(':'); + return [uid, editKey, password]; + } catch (error) { + return null; + } +} + +async function generateCredentials() { + const uid = shortid.generate(); + const editKey = generatePassword.generate({ + length: 20, + numbers: true, + }); + const password = ''; + return [uid, editKey, password]; +} + +async function saveCredentials(values) { + const contents = values.join(':'); + await createDir(path.join(CREDENTIALS_PATH, '..')); + return writeFile(CREDENTIALS_PATH, contents); +} + +function encodeEmail(uid) { + const encodedUid = encodeUid(uid); + return `${encodedUid}@firebase.com`; +} + +function signup([uid, editKey]) { + const emailVal = encodeEmail(uid); + const passwordVal = editKey; + return firebase.auth().createUserWithEmailAndPassword(emailVal, passwordVal); +} + +function signin([uid, editKey]) { + const emailVal = encodeEmail(uid); + const passwordVal = editKey; + return firebase.auth().signInWithEmailAndPassword(emailVal, passwordVal); +} + +module.exports = async () => { + let credentials = await readCredentials(); + if (!credentials) { + credentials = await generateCredentials(); + await saveCredentials(credentials); + await signup(credentials); + } + await signin(credentials); + return credentials; +}; diff --git a/src/firebase/index.js b/src/firebase/index.js index 0c5b324..72aba8f 100644 --- a/src/firebase/index.js +++ b/src/firebase/index.js @@ -1,12 +1,35 @@ const firebase = require('firebase'); const config = require('config'); +const log = require('../helpers/log'); + +const CronTask = require('../cron/CronTask'); +const auth = require('./auth'); +const server = require('./server'); +const queue = require('./queue'); const firebaseConfig = config.get('firebase'); function initialize() { firebase.initializeApp(firebaseConfig); + return auth(); +} + +function bgReportAlive(serverUid) { + const cron = new CronTask('Report Alive', '0,30 * * * * *', () => server.reportAlive(serverUid)); + cron.executeOnce(); + cron.start(); +} + +function bgListenQueue(serverUid) { + queue.listenQueue(serverUid); +} + +async function start() { + const [uid, , password] = await initialize(); + log.info(`[Firebase] Signed in as ${uid} (${password ? `password ${password}` : 'no password'})`); + + bgReportAlive(uid); + bgListenQueue(uid); } -module.exports = { - initialize, -}; +module.exports = { start }; diff --git a/src/firebase/queue.js b/src/firebase/queue.js new file mode 100644 index 0000000..431a0e1 --- /dev/null +++ b/src/firebase/queue.js @@ -0,0 +1,54 @@ +const firebase = require('firebase'); + +const log = require('../helpers/log'); +const { encodeUid } = require('./utils'); +const controllers = require('../controllers'); + +const { kml } = controllers; + +const SERVER_TIME = firebase.database.ServerValue.TIMESTAMP; + +const KML_VALUE = 'kml:value'; +const KML_HREF = 'kml:href'; +const QUERIES = 'queries'; + +const routes = value => ({ + [KML_VALUE]: () => kml.createKml({ contents: value }), + [KML_HREF]: () => kml.createKml({ uri: value }), + [QUERIES]: () => kml.createQuery({ contents: value }), +}); + +function controllerHandler(route, value) { + const routeAction = routes(value)[route]; + if (!routeAction) { + log.dev(`[Firebase] Unrecognised queue type: ${route}`); + return; + } + log.dev(`[Firebase] Executing queue type: ${route}`); + routeAction(); +} + +function listenQueue(uid) { + const encodedUid = encodeUid(uid); + const dbRef = firebase.database().ref(`queue/${encodedUid}`); + dbRef.orderByChild('timestamp').on('child_added', (snapshot) => { + const snapshotVal = snapshot.val(); + controllerHandler(snapshotVal.type, snapshotVal.value); + snapshot.ref.remove(); + }); +} + +function demoKml(uid) { + const encodedUid = encodeUid(uid); + const dbRef = firebase.database().ref(`queue/${encodedUid}`); + dbRef.push({ + type: KML_VALUE, + value: '123', + timestamp: SERVER_TIME, + }); +} + +module.exports = { + listenQueue, + demoKml, +}; diff --git a/src/firebase/server.js b/src/firebase/server.js new file mode 100644 index 0000000..9b29b07 --- /dev/null +++ b/src/firebase/server.js @@ -0,0 +1,25 @@ +const firebase = require('firebase'); +const publicIp = require('public-ip'); + +const { encodeUid } = require('./utils'); + +const SERVER_TIME = firebase.database.ServerValue.TIMESTAMP; + +async function reportAlive(uid) { + const encodedUid = encodeUid(uid); + const encodedPublicIp = (await publicIp.v4()).replace(/\./g, ':'); + + const serverRef = firebase.database().ref(`/servers/${encodedUid}`); + await serverRef.child('lastOnline').set(SERVER_TIME); + await serverRef.child('isOnline').set(true); + serverRef.child('isOnline').onDisconnect().set(false); + + const ipRef = firebase.database().ref(`/ips/${encodedPublicIp}/${encodedUid}`); + await ipRef.child('lastOnline').set(SERVER_TIME); + await ipRef.child('isOnline').set(true); + ipRef.child('isOnline').onDisconnect().set(false); +} + +module.exports = { + reportAlive, +}; diff --git a/src/firebase/utils.js b/src/firebase/utils.js new file mode 100644 index 0000000..17b738d --- /dev/null +++ b/src/firebase/utils.js @@ -0,0 +1,15 @@ +/** + * Uid is part of the firebase email name. This encoding makes sure to use only email valid + * characters. + * @param uid + */ +function encodeUid(uid) { + return uid.split('').reduce((prev, curr) => { + const encodedCurr = curr.charCodeAt(0); + return `${prev}${encodedCurr}`; + }, ''); +} + +module.exports = { + encodeUid, +}; diff --git a/src/server.js b/src/server.js index ff60410..7142557 100644 --- a/src/server.js +++ b/src/server.js @@ -5,12 +5,10 @@ const cookieParser = require('cookie-parser'); const morgan = require('morgan'); const Socketio = require('socket.io'); const config = require('config'); -const cors = require('cors'); const log = require('./helpers/log'); const routes = require('./routes'); const firebase = require('./firebase'); -const cron = require('./cron'); const socketConnectionHandler = require('./sockets'); const PORT = config.get('port'); @@ -20,13 +18,8 @@ const server = http.createServer(app); // Hey you! care about my order http://stackoverflow.com/a/16781554/2034015 -// Databases. -firebase.initialize(); - -// Cron jobs. -if (config.get('cronJobsEnabled')) { - cron.startAll(); -} +// Firebase stuff. +firebase.start(); // Cookies. app.use(cookieParser()); @@ -39,12 +32,10 @@ app.use(bodyParser.json()); app.use(morgan('combined', { stream: { write: msg => log.info(msg) } })); // URLs. -app.use(cors()); app.use('/', routes); // Socket.io const io = Socketio(server); -io.set('origins', '*:*'); io.on('connection', socketConnectionHandler); server.listen(PORT);