From 644fea6ba81e98f1fd9e9023735ed6361b79b39d Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Sun, 27 Feb 2022 23:59:24 -0800 Subject: [PATCH] Implementing websocket Signed-off-by: James Ketrenos --- client/package.json | 2 + client/src/Table.js | 28 ++++- client/src/setupProxy.js | 12 +++ server/app.js | 218 ++++++++------------------------------ server/http-server.js | 137 ------------------------ server/package.json | 3 +- server/routes/basepath.js | 2 +- 7 files changed, 88 insertions(+), 314 deletions(-) create mode 100644 client/src/setupProxy.js delete mode 100644 server/http-server.js diff --git a/client/package.json b/client/package.json index 8e931a5..54e7db0 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,9 @@ "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", + "http-proxy-middleware": "^2.0.3", "moment": "^2.29.1", + "moment-timezone": "^0.5.34", "react": "^17.0.2", "react-dom": "^17.0.2", "react-moment": "^1.1.1", diff --git a/client/src/Table.js b/client/src/Table.js index e2b22a7..4d1ba0f 100755 --- a/client/src/Table.js +++ b/client/src/Table.js @@ -917,7 +917,31 @@ class Table extends React.Component { componentDidMount() { this.start = new Date(); - const params = {}; + console.log(`Mounted: ${base}`); + let loc = window.location, new_uri; + if (loc.protocol === "https:") { + new_uri = "wss"; + } else { + new_uri = "ws"; + } + new_uri = `${new_uri}://${loc.host}${base}/ws`; + this.ws = new WebSocket(new_uri); + + this.ws.onopen = (event) => { + console.log(event); + //ws.send(JSON.stringify(apiCall)); + }; + + this.ws.onmessage = (event) => { + const json = JSON.parse(event.data); + console.log(json); + }; + + this.ws.onerror = (event) => { + console.error(event); + }; + + const params = {}; if (this.id) { console.log(`Loading game: ${this.id}`); params.url = `${base}/api/v1/games/${this.id}`; @@ -964,8 +988,6 @@ class Table extends React.Component { }).then((res) => { return res.json(); }).then((game) => { -// console.log (`Game ${game.id} loaded ${moment().format()}.`); - if (!this.id) { history.push(`${gamesPath}/${game.id}`); } diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js new file mode 100644 index 0000000..c17d3e5 --- /dev/null +++ b/client/src/setupProxy.js @@ -0,0 +1,12 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = function(app) { + const base = process.env.PUBLIC_URL; + console.log('http-proxy-middleware'); + app.use(createProxyMiddleware( + `${base}/ws`, { + ws: true, + target: 'http://localhost:8930', + changeOrigin: true, + })); +}; diff --git a/server/app.js b/server/app.js index 381265a..afce3dd 100755 --- a/server/app.js +++ b/server/app.js @@ -5,7 +5,6 @@ process.env.TZ = "Etc/GMT"; console.log("Loading ketr.ketran"); const express = require("express"), - morgan = require("morgan"), bodyParser = require("body-parser"), config = require("config"), session = require('express-session'), @@ -15,7 +14,6 @@ const express = require("express"), require("./console-line.js"); /* Monkey-patch console.log with line numbers */ - const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/", serverConfig = config.get("server"); @@ -25,51 +23,27 @@ let userDB, gameDB; const app = express(); -app.set("basePath", basePath); +app.use(bodyParser.json()); + /* App is behind an nginx proxy which we trust, so use the remote address * set in the headers */ app.set("trust proxy", true); +app.set("basePath", basePath); app.use(basePath, require("./routes/basepath.js")); /* Handle static files first so excessive logging doesn't occur */ app.use(basePath, express.static(frontendPath, { index: false })); -app.use(bodyParser.json()); +/* app.use(bodyParser.urlencoded({ extended: false })); - -/* ******************************************************************************* - * Logging - begin - * - * This runs before after cookie parsing, but before routes. If we set - * immediate: true on the morgan options, it happens before cookie parsing - * */ -const logging = false; - -if (logging) morgan.token('remote-user', function (req) { - return req.user ? req.user.username : "N/A"; -}); - -/* Any path starting with the following won't be logged via morgan */ -const logSkipPaths = new RegExp("^" + basePath + "(" + [ - ".*thumbs\\/", - "bower_components", -].join(")|(") + ")"); -if (logging) app.use(morgan('common', { - skip: function (req) { - return logSkipPaths.exec(req.originalUrl); - } -})); - -/* - * Logging - end - * ******************************************************************************* */ +*/ /* body-parser does not support text/*, so add support for that here */ -app.use(function(req, res, next){ +if (0) app.use(function(req, res, next){ if (!req.is('text/*')) { return next(); } @@ -84,13 +58,15 @@ app.use(function(req, res, next){ }); }); -app.use(session({ +const sessionParser = session({ store: new SQLiteStore({ db: config.get("sessions.db") }), secret: config.get("sessions.store-secret"), cookie: { maxAge: 21 * 24 * 60 * 60 * 1000 }, // 3 weeks saveUninitialized: false, resave: true -})); +}); + +app.use(sessionParser); const index = require("./routes/index"); @@ -99,146 +75,17 @@ if (config.has("admin")) { app.set("admin", admin); } -if (config.has("admin.mail") && - config.has("smtp.host") && - config.has("smtp.sender")) { - app.set("transporter", require("nodemailer").createTransport({ - host: config.get("smtp.host"), - pool: true, - port: config.has("smtp.port") ? config.get("smtp.port") : 25 - })); -} else { - console.log("SMTP disabled. To enable SMTP, configure admin.mail, smtp.host, and smtp.sender"); -} - -const templates = { - "html": [ - "

The user {{displayName}} has verified their email address ({{mail}}).

", - "", - "

They indicated they know:

", - "
{{notes}}
", - "", - "

To authenticate:

", - "

echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db

", - "", - "

Sincerely,
", - "James

" - ].join("\n"), - "text": [ - "The user {{displayName}} has verified their email address ({{mail}}).", - "", - "They indicated they know:", - "{{notes}}", - "", - "To authenticate:", - "echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db", - "", - "Sincerely,", - "James" - ].join("\n") -}; - -/* Look for action-token URLs and process; this does not require a user to be logged - * in */ -app.use(basePath, function(req, res, next) { - let match = req.url.match(/^\/([0-9a-f]+)$/); - if (!match) { - return next(); - } - - let key = match[1]; - return userDB.sequelize.query("SELECT * FROM authentications WHERE key=:key", { - replacements: { - key: key - }, - type: userDB.sequelize.QueryTypes.SELECT - }).then(function(results) { - let token; - if (results.length == 0) { - console.log("Invalid key. Ignoring."); - return next(); - } - - token = results[0]; - - console.log("Matched token: " + JSON.stringify(token, null, 2)); - switch (token.type) { - case "account-setup": - return userDB.sequelize.query("UPDATE users SET mailVerified=1 WHERE id=:userId", { - replacements: token - }).then(function() { - return userDB.sequelize.query("DELETE FROM authentications WHERE key=:key", { - replacements: token - }); - }).then(function() { - return userDB.sequelize.query("SELECT * FROM users WHERE id=:userId", { - replacements: token, - type: userDB.sequelize.QueryTypes.SELECT - }).then(function(results) { - if (results.length == 0) { - throw "DB mis-match between authentications and users table"; - } - const transporter = app.get("transporter"); - if (!transporter) { - console.log("Not sending VERIFIED email; SMTP not configured."); - return; - } - - let user = results[0], - envelope = { - to: config.get("admin.mail"), - from: config.get("smtp.sender"), - subject: "VERIFIED: Account'" + user.displayName + "'", - cc: "", - bcc: "", - text: hb.compile(templates.text)(user), - html: hb.compile(templates.html)(user) - }; - - req.session.userId = user.id; - - return new Promise(function (resolve, reject) { - let attempts = 10; - - function send(envelope) { - /* Rate limit to ten per second */ - transporter.sendMail(envelope, function (error, info) { - if (!error) { - console.log('Message sent: ' + info.response); - return resolve(); - } - - if (attempts == 0) { - console.log("Error sending email: ", error) - return reject(error); - } - - attempts--; - console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error); - setTimeout(send.bind(undefined, envelope), 100); - }); - } - - send(envelope); - }); - }).then(function() { - return res.redirect(308, basePath); - }); - }); - } - - return next(); - }); -}); - /* Allow loading of the app w/out being logged in */ app.use(basePath, index); + /* /games loads the default index */ app.use(basePath + "games", index); /* Allow access to the 'users' API w/out being logged in */ +/* const users = require("./routes/users"); app.use(basePath + "api/v1/users", users.router); +*/ app.use(function(err, req, res, next) { console.error(err.message); @@ -266,7 +113,6 @@ app.use(basePath, function(req, res, next) { /* Everything below here requires a successful authentication */ app.use(basePath, express.static(frontendPath, { index: false })); - app.use(basePath + "api/v1/games", require("./routes/games")); /* Declare the "catch all" index route last; the final route is a 404 dynamic router */ @@ -278,6 +124,7 @@ app.use(basePath, index); app.set("port", serverConfig.port); const server = require("http").createServer(app); + process.on('SIGINT', () => { server.close(() => { console.log("Gracefully shutting down from SIGINT (Ctrl-C)"); @@ -285,6 +132,8 @@ process.on('SIGINT', () => { }); }); +const WebSocket = require('ws'); + require("./db/games").then(function(db) { gameDB = db; }).then(function() { @@ -293,7 +142,36 @@ require("./db/games").then(function(db) { }); }).then(function() { console.log("DB connected. Opening server."); - server.listen(serverConfig.port); + + /* Create web socket server on top of a regular http server */ + const ws = new WebSocket.Server({ + server + }); + + /* Mount the Express app here */ + + //server.on('request', app); + + app.set('ws', ws); + + ws.on('connection', (req) => {/* + sessionParser(req.upgradeReq, {}, () => { + console.log("New websocket connection:"); + var sess = req.upgradeReq.session; + console.log("working = " + sess.working); + });*/ + }); + + ws.on('message', (message) => { + console.log(`received: ${message}`); + ws.send(JSON.stringify({ + answer: 42 + })); + }); + + server.listen(serverConfig.port, () => { + console.log(`http/ws server listening on ${serverConfig.port}`); + }); }).catch(function(error) { console.error(error); process.exit(-1); @@ -318,7 +196,3 @@ server.on("error", function(error) { throw error; } }); - -server.on("listening", function() { - console.log("Listening on " + serverConfig.port); -}); diff --git a/server/http-server.js b/server/http-server.js deleted file mode 100644 index 8ad8e46..0000000 --- a/server/http-server.js +++ /dev/null @@ -1,137 +0,0 @@ - -"use strict"; - -const express = require('express'), - morgan = require('morgan'), - cookieParser = require('cookie-parser'), - bodyParser = require('body-parser'), - http = require('http'), - config = require('config'), - app = express(), - { timestamp } = require('./timestamp'), - fs = require('fs'), - util = require('util'), - mkdir = util.promisify(fs.mkdir), - unlink = util.promisify(fs.unlink), - path = require('path'), - fetch = require('node-fetch'), - Promise = require('bluebird'), - url = require('url'), - { exec } = require('child_process'); - -fetch.Promise = Promise; - -const basePath = "/" + config.get("http.base").replace(/^\/*/, "").replace(/\/*$/, "") + "/", - dataPath = "/" + config.get("dataPath").replace(/^\/*/, "").replace(/\/*$/, "") + "/"; -/* */ - -if (!config.has("auth.idsid") || !config.has("auth.password")) { - console.error("You need to provide credentials to connect to ubit-gfx in config/local.json"); - console.error(' "auth": { "idsid": "USERNAME", "password": "PASSWORD" }'); - process.exit(-1); -} - -app.use(morgan('common')); - -app.use(bodyParser.json({ - verify: function(req,res,buf) { - req.rawBody = buf; - } -})); - -app.use(bodyParser.urlencoded({ - extended: false -})); -app.use(cookieParser()); - -/* Routes: - * /api/v1/publish Publish content to repository - */ - -app.get("/*", (req, res, next) => { - /* */ - return res.status(400).send({ usage: `POST ${basePath}api/v1/publish/:distro/:releaseStream/:url` }); -}); - -const auth = new Buffer(config.get("auth.idsid") + ":" + config.get("auth.password"), 'ascii').toString('base64'); - -app.post(basePath + 'api/v1/publish/:distro/:releaseStream/:url', function (req, res, next) { - const distro = req.params.distro, - releaseStream = req.params.releaseStream, - remoteUrl = req.params.url; - let filename; - - try { - filename = path.basename(url.parse(remoteUrl).pathname); - } catch (error) { - return res.status(400).send({ error: `Unparsable URL: ${remoteUrl}` }); - } - - if (distro.match(/\//) || releaseStream.match(/\//)) { - return res.status(400).send({ error: "Neither distro nor releaseStream may contain '/'" }); - } - - console.log(`POST publish/${distro}-${releaseStream}/${filename}`); - - const filepath = `${dataPath}${distro}-${releaseStream}`; - - return mkdir(filepath, { recursive: true }, () => { - const pathname = `${filepath}/${filename}`; - if (fs.existsSync(pathname)) { - return res.status(409).send({ message: `'${distro}-${releaseStream}/${filename}' already exists.` }); - } - - return fetch(remoteUrl, { - method: "GET", - headers: { - 'Authorization': `Basic ${auth}` - } - }).then(result => { - const dest = fs.createWriteStream(pathname); - dest.on('finish', () => { - exec(`./update-repository.sh ${distro}-${releaseStream}`, { - cwd: ".." , - shell: "/bin/bash" - }, (error, stdout, stderr) => { - if (error) { - return unlink(pathname).catch(() => { - console.error(`Unable to remove ${pathname} after update-repository.sh failed.`); - }).then(() => { - return res.status(500).send({ message: "Error while updating aptly database.", error: error, stderr: stderr, stdout: stdout }); - }); - } - return res.status(200).send({ message: "OK", stdout: stdout || "", stderr: stderr || "" }); - }); - }); - result.body.pipe(dest); - }).catch((error )=> { - const message = `Unable to download ${remoteUrl}: ${error}`; - console.error(message); - return res.status(500).send({ message: message }); - }); - }).catch((error) => { - const message = `Unable to mkdir ${filepath}: ${error}`; - console.error(message); - return res.status(500).send({ message: message }); - }); -}); - -app.post("/*", (req, res, next) => { - /* */ - return res.status(400).send({ usage: `POST /${basePath}/api/v1/publish/:distro/:releaseStream/:url` }); -}); - - -const server = http.createServer(app), - port = config.has("port") ? config.get("port") : 6543; - -server.listen(port); -server.on('listening', function() { - let addr = server.address(); - let bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - console.log(timestamp() + ` Now serving ${basePath} on ${bind}`); -}); - -module.exports = server; diff --git a/server/package.json b/server/package.json index a1b1a90..d583eac 100644 --- a/server/package.json +++ b/server/package.json @@ -26,7 +26,8 @@ "random-words": "^1.1.2", "sequelize": "^5.21.6", "sqlite3": "^4.1.1", - "typeface-roboto": "0.0.75" + "typeface-roboto": "0.0.75", + "ws": "^8.5.0" }, "repository": { "type": "git", diff --git a/server/routes/basepath.js b/server/routes/basepath.js index a8fb0bb..eba896d 100644 --- a/server/routes/basepath.js +++ b/server/routes/basepath.js @@ -8,7 +8,7 @@ const router = express.Router(); /* This router only handles HTML files and is used * to replace BASEPATH */ -router.get("/*", function(req, res, next) { +router.get("/*", (req, res, next) => { const parts = url.parse(req.url), basePath = req.app.get("basePath");