Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
*.user*
|
16
README.md
Normal file
16
README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# less_shitty_proxyjs implementation using redis and traefik
|
||||||
|
|
||||||
|
Install the traefik proxyjs.yml file (using the file provider!) to register the default handler that does load balancing to all proxyjs instances
|
||||||
|
Install redis in traefik, to allow for dynamic route updates
|
||||||
|
|
||||||
|
## Build and run
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://code.brunner.ninja/feedc0de/less_shitty_proxyjs.git
|
||||||
|
cd less_shitty_proxyjs/
|
||||||
|
mkdir build
|
||||||
|
cd build/
|
||||||
|
qmake6 ..
|
||||||
|
make -j8
|
||||||
|
parallel -j10 --linebuffer ./less_shitty_proxyjs 'test{}' '950{}' ::: {0..9}
|
||||||
|
```
|
86
client.cpp
Normal file
86
client.cpp
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#include "client.h"
|
||||||
|
|
||||||
|
#include <QWebSocket>
|
||||||
|
|
||||||
|
#include "webserver.h"
|
||||||
|
|
||||||
|
Client::Client(WebServer &server, std::unique_ptr<QWebSocket> &&socket,
|
||||||
|
const QString &serial, const std::set<std::unique_ptr<Client>> &serialClients) :
|
||||||
|
QObject{&server},
|
||||||
|
m_server{server},
|
||||||
|
m_socket{std::move(socket)},
|
||||||
|
m_serial{serial}
|
||||||
|
{
|
||||||
|
qDebug() << "new ws connection!!" << m_socket->requestUrl();
|
||||||
|
|
||||||
|
m_socket->sendTextMessage(QString{"Hello from server %0, you requested serial %1, there are %2 clients connected"}
|
||||||
|
.arg(m_server.m_identity, serial).arg(serialClients.size()));
|
||||||
|
|
||||||
|
for (auto &other : serialClients)
|
||||||
|
{
|
||||||
|
other->sendTextMessage(QString{"A new client connected, number of connected clients: %0"}.arg(serialClients.size() + 1));
|
||||||
|
|
||||||
|
connect(this, &Client::sendTextMessageToOthers,
|
||||||
|
other.get(), &Client::sendTextMessage);
|
||||||
|
connect(other.get(), &Client::sendTextMessageToOthers,
|
||||||
|
this, &Client::sendTextMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
QObject::connect(m_socket.get(), &QWebSocket::disconnected,
|
||||||
|
this, &Client::socketDisconnected);
|
||||||
|
|
||||||
|
QObject::connect(m_socket.get(), &QWebSocket::destroyed,
|
||||||
|
this, &Client::socketDestroyed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::~Client() = default;
|
||||||
|
|
||||||
|
void Client::sendTextMessage(const QString &text)
|
||||||
|
{
|
||||||
|
qDebug() << text;
|
||||||
|
if (m_socket)
|
||||||
|
m_socket->sendTextMessage(text);
|
||||||
|
else
|
||||||
|
qWarning() << "tried to send with invalid socket";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::textMessageReceived(const QString &text)
|
||||||
|
{
|
||||||
|
qDebug() << text;
|
||||||
|
emit sendTextMessageToOthers(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::socketDestroyed()
|
||||||
|
{
|
||||||
|
qDebug() << "destroyed";
|
||||||
|
m_socket.release();
|
||||||
|
assert(!m_socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Client::socketDisconnected()
|
||||||
|
{
|
||||||
|
qDebug() << "disconnected";
|
||||||
|
|
||||||
|
std::set<std::unique_ptr<Client>> &serialClients = m_server.m_clients[m_serial];
|
||||||
|
|
||||||
|
{
|
||||||
|
auto iter = std::find_if(std::begin(serialClients), std::end(serialClients), [this](const auto &ptr){
|
||||||
|
return ptr.get() == this;
|
||||||
|
});
|
||||||
|
if (iter == std::end(serialClients))
|
||||||
|
qWarning() << "couldnt fint ourself when trying to erase us";
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qDebug() << serialClients.size();
|
||||||
|
serialClients.extract(iter).value().release()->deleteLater();
|
||||||
|
qDebug() << serialClients.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serialClients.empty())
|
||||||
|
m_server.destroyTraefikRoute(m_serial);
|
||||||
|
else for (auto &ptr : serialClients)
|
||||||
|
ptr->sendTextMessage(QString{"A client disconnected, number of remaining connected clients: %0"}.arg(serialClients.size()));
|
||||||
|
|
||||||
|
qDebug() << "done with sending...";
|
||||||
|
}
|
35
client.h
Normal file
35
client.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
class WebServer;
|
||||||
|
class QWebSocket;
|
||||||
|
|
||||||
|
class Client : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Client(WebServer &server, std::unique_ptr<QWebSocket> &&socket,
|
||||||
|
const QString &serial, const std::set<std::unique_ptr<Client>> &serialClients);
|
||||||
|
~Client() override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void sendTextMessageToOthers(const QString &text);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void sendTextMessage(const QString &text);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void textMessageReceived(const QString &text);
|
||||||
|
void socketDestroyed();
|
||||||
|
void socketDisconnected();
|
||||||
|
|
||||||
|
private:
|
||||||
|
WebServer &m_server;
|
||||||
|
std::unique_ptr<QWebSocket> m_socket;
|
||||||
|
const QString m_serial;
|
||||||
|
};
|
29
client.html
Normal file
29
client.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>less shitty proxyjs</title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous" />
|
||||||
|
<link href="style.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>client.html</h1>
|
||||||
|
<p><b>Server-Identity:</b> {{identity}}</p>
|
||||||
|
<p><b>Request-Serial:</b> {{serial}}</p>
|
||||||
|
<p><b>WS status:</b> <span id="wsStatus">Disconnected</span></p>
|
||||||
|
<button id="reconnectButton">Connect</button>
|
||||||
|
|
||||||
|
<div id="logView"></div>
|
||||||
|
<form id="sendForm">
|
||||||
|
<label>Text to send: <input type="text" required /></label>
|
||||||
|
<button type="submit">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q" crossorigin="anonymous"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>less shitty proxyjs</title>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>index.html</h1>
|
||||||
|
<p><b>Server-Identity:</b> {{identity}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q" crossorigin="anonymous"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
28
less_shitty_proxyjs.pro
Normal file
28
less_shitty_proxyjs.pro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
QT = core network websockets httpserver
|
||||||
|
|
||||||
|
CONFIG += c++latest cmdline
|
||||||
|
|
||||||
|
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
client.cpp \
|
||||||
|
main.cpp \
|
||||||
|
redisqtadapter.cpp \
|
||||||
|
webserver.cpp
|
||||||
|
|
||||||
|
RESOURCES += \
|
||||||
|
resources.qrc
|
||||||
|
|
||||||
|
OTHER_FILES += \
|
||||||
|
client.html \
|
||||||
|
index.html \
|
||||||
|
script.js \
|
||||||
|
style.css
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
client.h \
|
||||||
|
redisqtadapter.h \
|
||||||
|
webserver.h
|
||||||
|
|
||||||
|
LIBS += \
|
||||||
|
-L/usr/lib -lhiredis
|
85
main.cpp
Normal file
85
main.cpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
|
||||||
|
#include "webserver.h"
|
||||||
|
|
||||||
|
#include <QTcpServer>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
qSetMessagePattern("%{time dd.MM.yyyy HH:mm:ss.zzz} "
|
||||||
|
"["
|
||||||
|
"%{if-debug}D%{endif}"
|
||||||
|
"%{if-info}I%{endif}"
|
||||||
|
"%{if-warning}W%{endif}"
|
||||||
|
"%{if-critical}C%{endif}"
|
||||||
|
"%{if-fatal}F%{endif}"
|
||||||
|
"] "
|
||||||
|
"%{function}(): "
|
||||||
|
"%{message}");
|
||||||
|
|
||||||
|
QCoreApplication app{argc, argv};
|
||||||
|
QCoreApplication::setApplicationName("less_shitty_proxyjs");
|
||||||
|
QCoreApplication::setApplicationVersion("1.0");
|
||||||
|
|
||||||
|
QString identity = "test";
|
||||||
|
uint16_t port = 8000;
|
||||||
|
|
||||||
|
{
|
||||||
|
QCommandLineParser parser;
|
||||||
|
parser.setApplicationDescription("Test helper");
|
||||||
|
parser.addHelpOption();
|
||||||
|
parser.addVersionOption();
|
||||||
|
parser.addPositionalArgument("identiy", QCoreApplication::translate("main", "The name of this service instance"));
|
||||||
|
parser.addPositionalArgument("port", QCoreApplication::translate("main", "The port to listen on (will only listen on localhost)."));
|
||||||
|
parser.process(app);
|
||||||
|
|
||||||
|
const auto &args = parser.positionalArguments();
|
||||||
|
if (args.size() > 0)
|
||||||
|
identity = args.first();
|
||||||
|
if (args.size() > 1)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
port = args.at(1).toInt(&ok);
|
||||||
|
if (!ok)
|
||||||
|
{
|
||||||
|
qCritical("could not parse port: %s", qPrintable(args.at(1)));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qSetMessagePattern(QString{"%{time dd.MM.yyyy HH:mm:ss.zzz} %0 "
|
||||||
|
"["
|
||||||
|
"%{if-debug}D%{endif}"
|
||||||
|
"%{if-info}I%{endif}"
|
||||||
|
"%{if-warning}W%{endif}"
|
||||||
|
"%{if-critical}C%{endif}"
|
||||||
|
"%{if-fatal}F%{endif}"
|
||||||
|
"] "
|
||||||
|
"%{function}(): "
|
||||||
|
"%{message}"}.arg(identity));
|
||||||
|
|
||||||
|
WebServer server{identity, QString{"http://localhost:%0"}.arg(port)};
|
||||||
|
|
||||||
|
QTcpServer tcpServer;
|
||||||
|
if (!tcpServer.listen(QHostAddress::LocalHost, port))
|
||||||
|
{
|
||||||
|
qCritical("failed to start listening on port %hu: %s", port, qPrintable(tcpServer.errorString()));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.bind(&tcpServer))
|
||||||
|
{
|
||||||
|
qCritical("failed to webserver on socket!");
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "server started";
|
||||||
|
|
||||||
|
auto result = app.exec();
|
||||||
|
qDebug() << "bey bey!";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
25
proxyjs.yml
Normal file
25
proxyjs.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
proxyjs:
|
||||||
|
rule: "Host(`proxyjs.brunner.ninja`)"
|
||||||
|
service: proxyjs
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
tls:
|
||||||
|
certResolver: myresolver
|
||||||
|
priority: 10
|
||||||
|
|
||||||
|
services:
|
||||||
|
proxyjs:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://localhost:9500"
|
||||||
|
- url: "http://localhost:9501"
|
||||||
|
- url: "http://localhost:9502"
|
||||||
|
- url: "http://localhost:9503"
|
||||||
|
- url: "http://localhost:9504"
|
||||||
|
- url: "http://localhost:9505"
|
||||||
|
- url: "http://localhost:9506"
|
||||||
|
- url: "http://localhost:9507"
|
||||||
|
- url: "http://localhost:9508"
|
||||||
|
- url: "http://localhost:9509"
|
2
redisqtadapter.cpp
Normal file
2
redisqtadapter.cpp
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#include "redisqtadapter.h"
|
||||||
|
|
108
redisqtadapter.h
Normal file
108
redisqtadapter.h
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSocketNotifier>
|
||||||
|
#include <hiredis/async.h>
|
||||||
|
|
||||||
|
static void RedisQtAddRead(void *);
|
||||||
|
static void RedisQtDelRead(void *);
|
||||||
|
static void RedisQtAddWrite(void *);
|
||||||
|
static void RedisQtDelWrite(void *);
|
||||||
|
static void RedisQtCleanup(void *);
|
||||||
|
|
||||||
|
class RedisQtAdapter : public QObject {
|
||||||
|
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
friend
|
||||||
|
void RedisQtAddRead(void * adapter) {
|
||||||
|
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||||
|
a->addRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
friend
|
||||||
|
void RedisQtDelRead(void * adapter) {
|
||||||
|
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||||
|
a->delRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
friend
|
||||||
|
void RedisQtAddWrite(void * adapter) {
|
||||||
|
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||||
|
a->addWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
friend
|
||||||
|
void RedisQtDelWrite(void * adapter) {
|
||||||
|
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||||
|
a->delWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
friend
|
||||||
|
void RedisQtCleanup(void * adapter) {
|
||||||
|
RedisQtAdapter * a = static_cast<RedisQtAdapter *>(adapter);
|
||||||
|
a->cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
RedisQtAdapter(QObject * parent = 0)
|
||||||
|
: QObject(parent), m_ctx(0), m_read(0), m_write(0) { }
|
||||||
|
|
||||||
|
~RedisQtAdapter() {
|
||||||
|
if (m_ctx != 0) {
|
||||||
|
m_ctx->ev.data = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int setContext(redisAsyncContext * ac) {
|
||||||
|
if (ac->ev.data != NULL) {
|
||||||
|
return REDIS_ERR;
|
||||||
|
}
|
||||||
|
m_ctx = ac;
|
||||||
|
m_ctx->ev.data = this;
|
||||||
|
m_ctx->ev.addRead = RedisQtAddRead;
|
||||||
|
m_ctx->ev.delRead = RedisQtDelRead;
|
||||||
|
m_ctx->ev.addWrite = RedisQtAddWrite;
|
||||||
|
m_ctx->ev.delWrite = RedisQtDelWrite;
|
||||||
|
m_ctx->ev.cleanup = RedisQtCleanup;
|
||||||
|
return REDIS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void addRead() {
|
||||||
|
if (m_read) return;
|
||||||
|
m_read = new QSocketNotifier(m_ctx->c.fd, QSocketNotifier::Read, 0);
|
||||||
|
connect(m_read, SIGNAL(activated(int)), this, SLOT(read()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void delRead() {
|
||||||
|
if (!m_read) return;
|
||||||
|
delete m_read;
|
||||||
|
m_read = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addWrite() {
|
||||||
|
if (m_write) return;
|
||||||
|
m_write = new QSocketNotifier(m_ctx->c.fd, QSocketNotifier::Write, 0);
|
||||||
|
connect(m_write, SIGNAL(activated(int)), this, SLOT(write()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void delWrite() {
|
||||||
|
if (!m_write) return;
|
||||||
|
delete m_write;
|
||||||
|
m_write = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup() {
|
||||||
|
delRead();
|
||||||
|
delWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void read() { redisAsyncHandleRead(m_ctx); }
|
||||||
|
void write() { redisAsyncHandleWrite(m_ctx); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
redisAsyncContext * m_ctx;
|
||||||
|
QSocketNotifier * m_read;
|
||||||
|
QSocketNotifier * m_write;
|
||||||
|
};
|
8
resources.qrc
Normal file
8
resources.qrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/lspjs">
|
||||||
|
<file>client.html</file>
|
||||||
|
<file>index.html</file>
|
||||||
|
<file>script.js</file>
|
||||||
|
<file>style.css</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
63
script.js
Normal file
63
script.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
jQuery(document).ready(function($){
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
$('#reconnectButton').click(function(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (ws) {
|
||||||
|
$('#reconnectButton').text('Connect');
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
} else {
|
||||||
|
$('#reconnectButton').text('Disconnect');
|
||||||
|
setWsStatus('Connecting');
|
||||||
|
ws = new WebSocket(window.location.href.replace(/^http/, "ws"));
|
||||||
|
ws.onopen = function(event) {
|
||||||
|
setWsStatus('Connected');
|
||||||
|
$('#sendForm input').focus();
|
||||||
|
}
|
||||||
|
ws.onclose = function(event) {
|
||||||
|
setWsStatus('Closed');
|
||||||
|
$('#reconnectButton').text('Connect');
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
ws.onerror = function(event) {
|
||||||
|
setWsStatus('Error');
|
||||||
|
}
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
if (typeof event.data == 'string') {
|
||||||
|
pushLine('Received ' + event.data);
|
||||||
|
} else if (typeof event.data == 'object') {
|
||||||
|
pushLine('Received binary message!');
|
||||||
|
} else {
|
||||||
|
pushLine('Unknown msg data type ' + typeof event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}).click();
|
||||||
|
|
||||||
|
$('#sendForm').submit(function(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (!ws) {
|
||||||
|
alert('please connect first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = $('#sendForm input');
|
||||||
|
const val = input.val();
|
||||||
|
pushLine('Sending ' + val);
|
||||||
|
ws.send(val);
|
||||||
|
input.val('').focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function pushLine(line) {
|
||||||
|
$('#logView').append($('<p>').text(new Date().toLocaleTimeString() + ': ' + line));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWsStatus(status) {
|
||||||
|
$('#wsStatus').text(status);
|
||||||
|
pushLine('Status changed to ' + status);
|
||||||
|
}
|
6
style.css
Normal file
6
style.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#logView {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
166
webserver.cpp
Normal file
166
webserver.cpp
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#include "webserver.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QMimeDatabase>
|
||||||
|
|
||||||
|
#include "client.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QHttpServerResponse serveHtmlWithPlaceholders(const QString &filePath, const QMap<QString, QString> &placeholders);
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WebServer::WebServer(const QString &identity, const QString &url, QObject *parent) :
|
||||||
|
QObject{parent},
|
||||||
|
m_identity{identity}
|
||||||
|
{
|
||||||
|
m_redis = redisAsyncConnect("localhost", 6379);
|
||||||
|
|
||||||
|
if (m_redis->err)
|
||||||
|
qFatal("error with redis: %s", m_redis->errstr);
|
||||||
|
|
||||||
|
m_redisAdapter.setContext(m_redis);
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "SET %s %s",
|
||||||
|
QString{"traefik/http/services/proxyjs_%0/loadbalancer/servers/0/url"}.arg(m_identity).toUtf8().constData(),
|
||||||
|
url.toUtf8().constData());
|
||||||
|
|
||||||
|
m_server.route("/", [&]() {
|
||||||
|
return serveHtmlWithPlaceholders(":/lspjs/index.html", {
|
||||||
|
{"identity", identity}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route("/<arg>", [&](int serial) {
|
||||||
|
return serveHtmlWithPlaceholders(":/lspjs/client.html", {
|
||||||
|
{"identity", identity},
|
||||||
|
{"serial", QString::number(serial)}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route("/script.js", []() {
|
||||||
|
return QHttpServerResponse::fromFile(":/lspjs/script.js");
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.route("/style.css", []() {
|
||||||
|
return QHttpServerResponse::fromFile(":/lspjs/style.css");
|
||||||
|
});
|
||||||
|
|
||||||
|
m_server.addWebSocketUpgradeVerifier(this, &WebServer::verifySocketUpgrade);
|
||||||
|
|
||||||
|
QObject::connect(&m_server, &QHttpServer::newWebSocketConnection,
|
||||||
|
this, qOverload<>(&WebServer::newWebSocketConnection));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebServer::bind(QTcpServer *server)
|
||||||
|
{
|
||||||
|
return m_server.bind(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebServer::~WebServer() = default;
|
||||||
|
|
||||||
|
void WebServer::newWebSocketConnection()
|
||||||
|
{
|
||||||
|
while (auto socket = m_server.nextPendingWebSocketConnection())
|
||||||
|
newWebSocketConnection(std::move(socket));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServer::createTraefikRoute(const QString &serial)
|
||||||
|
{
|
||||||
|
qDebug() << "create traefik route for" << serial << "to" << m_identity;
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "SET %s %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/rule"}.arg(serial).toUtf8().constData(),
|
||||||
|
QString{"Host(`proxyjs.brunner.ninja`) && Path(`/%0`)"}.arg(serial).toUtf8().constData());
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "SET %s %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/entrypoints/0"}.arg(serial).toUtf8().constData(),
|
||||||
|
"websecure");
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "SET %s %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/service"}.arg(serial).toUtf8().constData(),
|
||||||
|
QString{"proxyjs_%0"}.arg(m_identity).toUtf8().constData());
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "SET %s %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/tls/certresolver"}.arg(serial).toUtf8().constData(),
|
||||||
|
"myresolver");
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "SET %s %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/priority"}.arg(serial).toUtf8().constData(),
|
||||||
|
"15");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServer::destroyTraefikRoute(const QString &serial)
|
||||||
|
{
|
||||||
|
qDebug() << "destroy traefik route for" << serial << "to" << m_identity;
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "DEL %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/rule"}.arg(serial).toUtf8().constData());
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "DEL %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/entrypoints/0"}.arg(serial).toUtf8().constData());
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "DEL %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/service"}.arg(serial).toUtf8().constData());
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "DEL %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/tls/certresolver"}.arg(serial).toUtf8().constData());
|
||||||
|
|
||||||
|
redisAsyncCommand(m_redis, NULL, NULL, "DEL %s",
|
||||||
|
QString{"traefik/http/routers/proxyjs_%0/priority"}.arg(serial).toUtf8().constData());
|
||||||
|
}
|
||||||
|
|
||||||
|
QHttpServerWebSocketUpgradeResponse WebServer::verifySocketUpgrade(const QHttpServerRequest &request)
|
||||||
|
{
|
||||||
|
auto path = request.url().path();
|
||||||
|
while (path.startsWith('/'))
|
||||||
|
path.removeFirst();
|
||||||
|
bool ok;
|
||||||
|
path.toInt(&ok);
|
||||||
|
if (!ok)
|
||||||
|
return QHttpServerWebSocketUpgradeResponse::passToNext();
|
||||||
|
|
||||||
|
return QHttpServerWebSocketUpgradeResponse::accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebServer::newWebSocketConnection(std::unique_ptr<QWebSocket> &&socket)
|
||||||
|
{
|
||||||
|
auto serial = socket->requestUrl().path();
|
||||||
|
if (serial.startsWith('/'))
|
||||||
|
serial.removeFirst();
|
||||||
|
|
||||||
|
auto &set = m_clients[serial];
|
||||||
|
if (set.empty())
|
||||||
|
createTraefikRoute(serial);
|
||||||
|
|
||||||
|
set.emplace(std::make_unique<Client>(*this, std::move(socket), serial, set));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QHttpServerResponse serveHtmlWithPlaceholders(const QString &filePath, const QMap<QString, QString> &placeholders)
|
||||||
|
{
|
||||||
|
QString content;
|
||||||
|
|
||||||
|
{
|
||||||
|
QFile file{filePath};
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
|
qWarning("could not open file %s because %s", qPrintable(filePath), qPrintable(file.errorString()));
|
||||||
|
return QHttpServerResponse{"Failed to open file", QHttpServerResponder::StatusCode::InternalServerError};
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextStream in(&file);
|
||||||
|
content = in.readAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString modified = content;
|
||||||
|
for (auto it = placeholders.begin(); it != placeholders.end(); ++it)
|
||||||
|
modified.replace("{{" + it.key() + "}}", it.value());
|
||||||
|
|
||||||
|
QMimeDatabase db;
|
||||||
|
QMimeType mime = db.mimeTypeForFile(filePath);
|
||||||
|
|
||||||
|
QHttpServerResponse response(mime.name().toUtf8(), modified.toUtf8());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} // namespace
|
45
webserver.h
Normal file
45
webserver.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QHttpServer>
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <set>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include <hiredis/async.h>
|
||||||
|
#include "redisqtadapter.h"
|
||||||
|
|
||||||
|
class QWebSocket;
|
||||||
|
class Client;
|
||||||
|
|
||||||
|
class WebServer : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
friend class Client;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WebServer(const QString &identity, const QString &url, QObject *parent = nullptr);
|
||||||
|
~WebServer() override;
|
||||||
|
|
||||||
|
bool bind(QTcpServer *server);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void newWebSocketConnection();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void createTraefikRoute(const QString &serial);
|
||||||
|
void destroyTraefikRoute(const QString &serial);
|
||||||
|
|
||||||
|
QHttpServerWebSocketUpgradeResponse verifySocketUpgrade(const QHttpServerRequest &request);
|
||||||
|
void newWebSocketConnection(std::unique_ptr<QWebSocket> &&socket);
|
||||||
|
|
||||||
|
QHttpServer m_server;
|
||||||
|
|
||||||
|
QString m_identity;
|
||||||
|
|
||||||
|
std::unordered_map<QString, std::set<std::unique_ptr<Client>>> m_clients;
|
||||||
|
|
||||||
|
redisAsyncContext *m_redis;
|
||||||
|
RedisQtAdapter m_redisAdapter;
|
||||||
|
};
|
Reference in New Issue
Block a user