diff --git a/src/3proxy-eagle.pro b/src/3proxy-eagle.pro new file mode 100644 index 0000000..5fe064b --- /dev/null +++ b/src/3proxy-eagle.pro @@ -0,0 +1,33 @@ +QT -= gui +QT += network + +CONFIG += c++17 console +CONFIG -= app_bundle + +SOURCES += \ + dailycounter.cpp \ + g.cpp \ + httpdocument.cpp \ + httpserver.cpp \ + main.cpp \ + proxyinstanse.cpp \ + socketrunnable.cpp \ + socketrunnablebase.cpp \ + statistics.cpp \ + webpage.cpp \ + webserverbase.cpp + +HEADERS += \ + dailycounter.h \ + g.h \ + httpdocument.h \ + httpserver.h \ + proxyinstanse.h \ + socketrunnable.h \ + socketrunnablebase.h \ + statistics.h \ + webpage.h \ + webserverbase.h + +RESOURCES += \ + html-static.qrc diff --git a/src/dailycounter.cpp b/src/dailycounter.cpp new file mode 100644 index 0000000..420e3ff --- /dev/null +++ b/src/dailycounter.cpp @@ -0,0 +1,86 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "dailycounter.h" + +#include + +DailyCounter::DailyCounter() +{ +} + +quint64 DailyCounter::value() +{ + const auto factDay = getCurrentDay(); + if (day() != factDay) + { + m_day = factDay; + m_counter = 0; + } + return m_counter; +} + +QString DailyCounter::day() const +{ + return m_day; +} + +QJsonObject DailyCounter::serialize() +{ + QJsonObject me; + me["day"] = day(); + me["count"] = QString::number( value() ); + return me; +} + +void DailyCounter::deserialize(const QJsonObject &json) +{ + const QString serializedDay = json.value("day").toString(); + const QString factDay = getCurrentDay(); + + m_day = factDay; + auto serializedCounter = json.value("count").toString().toULongLong(); + if (serializedDay == factDay) + { + m_counter = serializedCounter; + } + else + { + m_counter = 0; + } +} + +void DailyCounter::increment(quint64 value) +{ + const auto factDay = getCurrentDay(); + if (day() == factDay) + { + m_counter += value; + } + else + { + m_day = factDay; + m_counter = value; + } +} + +QString DailyCounter::getCurrentDay() +{ + return QDateTime::currentDateTimeUtc().toString(Qt::DateFormat::RFC2822Date); +} diff --git a/src/dailycounter.h b/src/dailycounter.h new file mode 100644 index 0000000..b968a83 --- /dev/null +++ b/src/dailycounter.h @@ -0,0 +1,40 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +class DailyCounter +{ +public: + DailyCounter(); + quint64 value(); + QString day() const; + QJsonObject serialize(); + void deserialize(const QJsonObject& json); + void increment(quint64); + + static QString getCurrentDay(); + +private: + QString m_day; + std::atomic m_counter {0}; +}; diff --git a/src/g.cpp b/src/g.cpp new file mode 100644 index 0000000..95cf8bb --- /dev/null +++ b/src/g.cpp @@ -0,0 +1,212 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "g.h" + +#include +#include +#include +#include +#include +#include + +namespace g { + +namespace c { + const QString SOFTWARE_NAME = "3proxy-eagle"; + const QString SOFTWARE_VERSION = "0.0.1a"; + const QString COPYRIGHT = "GPLv3 (c) acetone, 2022"; + const QString HA_PAGE_TITLE = "{{PAGE_TITLE}}"; + const QString HA_CUSTOM_CSS = "{{CUSTOM_CSS}}"; + const QString HA_DAILY_UPLOAD = "{{DAILY_UPLOAD}}"; + const QString HA_DAILY_UPLOAD_MEASURE = "{{DAILY_UPLOAD_MEASURE}}"; + const QString HA_DAILY_DOWNLOAD = "{{DAILY_DOWNLOAD}}"; + const QString HA_DAILY_DOWNLOAD_MEASURE = "{{DAILY_DOWNLOAD_MEASURE}}"; + const QString HA_TOTAL_UPLOAD = "{{TOTAL_UPLOAD}}"; + const QString HA_TOTAL_DOWNLOAD = "{{TOTAL_DOWNLOAD}}"; + const QString HA_LAST_DESTINATIONS_LIST = "{{LAST_DESTINATIONS_LIST}}"; + const QString HA_DAILY_TOP_DESTINATIONS_LIST = "{{DAILY_TOP_DESTINATIONS_LIST}}"; + const QString HA_TOTAL_TOP_DESTINATIONS_LIST = "{{TOTAL_TOP_DESTINATIONS_LIST}}"; + const QString HA_BLOCKED_DESTINTIONS = "{{BLOCKED_DESTINATIONS}}"; + const QString HA_INFORMATION = "{{INFORMATION}}"; + const QString HA_COPYRIGHT = "{{COPYRIGHT}}"; +} // namespace c + +namespace p { + uint LAST_AND_TOP_LIST_SIZE = 5; + QStringList IGNORED_DESTINATIONS = {"[0.0.0.0]", "127.0.0.1"}; + QString WORKING_DIR = "data"; + QString SERVICE_TITLE = "3proxy-eagle"; + QString BIND_TO_ADDRESS = "127.0.0.1"; + quint16 BIND_TO_PORT = 8161; +} // namespace p + +std::list< std::pair > instanses; + +LogLevel logLevel = LogLevel::Info; + +void customMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (logLevel == LogLevel::Off) return; + + static QMutex mtx; + QMutexLocker lock (&mtx); + + QByteArray localMsg = msg.toLocal8Bit(); + const char *file = context.file ? context.file : ""; + const char *function = context.function ? context.function : ""; + + if (type == QtDebugMsg) + { + if (logLevel == LogLevel::Debug) + { + std::cout << "[Dbug]: " << localMsg.constData() << " " << function << std::endl; + } + } + + else if (type == QtInfoMsg) + { + if (logLevel >= LogLevel::Info) + { + std::cout << "[Info]: " << localMsg.constData() << std::endl; + } + } + + else if (type == QtWarningMsg) + { + if (logLevel >= LogLevel::Warning) + { + std::cerr << "[Warn]: " << localMsg.constData() << std::endl; + } + } + + else if (type == QtCriticalMsg) + { + if (logLevel >= LogLevel::Error) + { + std::cerr << "[Crit]: " << localMsg.constData() << " (" << file << "," << context.line << "," << function << ")" << std::endl; + } + } + + else if (type == QtFatalMsg) + { + if (logLevel >= LogLevel::Error) + { + std::cerr << "[Fatl]: " << localMsg.constData() << " (" << file << "," << context.line << "," << function << ")" << std::endl; + } + } +} + +QString getValue(const QString &string, const QString &key, GetValueType type) +{ + if (key.isEmpty()) + { + return QString(); + } + + if (type == GetValueType::HttpHeader) + { + if (string.indexOf(QRegularExpression(key + ":")) == -1) + { + return QString(); + } + } + else if (string.indexOf(QRegularExpression(key + "\\s*=")) == -1) + { + return QString(); + } + + QString result {string}; + if (type == GetValueType::HttpHeader) + { + result.remove(QRegularExpression("^.*"+key+":\\s", QRegularExpression::DotMatchesEverythingOption)); + } + else + { + result.remove(QRegularExpression("^.*"+key+"\\s*=", QRegularExpression::DotMatchesEverythingOption)); + } + + QString separator {' '}; + if (type == GetValueType::Url) + { + separator = '&'; + } + else if (type == GetValueType::HttpHeader) + { + separator = "\r\n"; + } + + int valueEnd = result.indexOf(separator); + + if (valueEnd == -1 and type == GetValueType::Url) + { + separator = ' '; + valueEnd = result.indexOf(separator); + if (valueEnd != -1) + { + result.remove(valueEnd, result.size()-valueEnd); + } + } + else if (valueEnd != -1) + { + result.remove(valueEnd, result.size()-valueEnd); + } + + if (type == GetValueType::Url) + { + result = QByteArray::fromPercentEncoding(result.toUtf8()); + result.replace('+', ' '); + } + else if (type == GetValueType::HttpHeader) + { + result.remove('\"'); + } + else + { + static QRegularExpression beginSpaces("^\\s*"); + static QRegularExpression endSpaces("\\s*$"); + result.remove(beginSpaces); + result.remove(endSpaces); + } + + return result; +} + + +QString hash(const QByteArray &data) +{ + QString hash = QCryptographicHash::hash(data, QCryptographicHash::Md5).toBase64(); + static QRegularExpression rgx_removeTrash ("[^0-9a-zA-Z_]"); + hash.remove(rgx_removeTrash); + return hash; +} + +QString hash(QFile file) +{ + if (not file.open(QIODevice::ReadOnly)) + { + qDebug() << "Hash failed: file openning failed:" << file.fileName(); + return QString(); + } + QString result = hash(file.readAll()); + file.close(); + return result; +} + +} // namespace g diff --git a/src/g.h b/src/g.h new file mode 100644 index 0000000..e1fb8a3 --- /dev/null +++ b/src/g.h @@ -0,0 +1,80 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "proxyinstanse.h" +#include + +class QFile; + +namespace g /* for Global */ { + +namespace c /* for Constants */ { + extern const QString SOFTWARE_NAME; + extern const QString SOFTWARE_VERSION; + extern const QString COPYRIGHT; + extern const QString HA_PAGE_TITLE; + extern const QString HA_CUSTOM_CSS; + extern const QString HA_DAILY_UPLOAD; + extern const QString HA_DAILY_UPLOAD_MEASURE; + extern const QString HA_DAILY_DOWNLOAD; + extern const QString HA_DAILY_DOWNLOAD_MEASURE; + extern const QString HA_TOTAL_UPLOAD; + extern const QString HA_TOTAL_DOWNLOAD; + extern const QString HA_LAST_DESTINATIONS_LIST; + extern const QString HA_DAILY_TOP_DESTINATIONS_LIST; + extern const QString HA_TOTAL_TOP_DESTINATIONS_LIST; + extern const QString HA_BLOCKED_DESTINTIONS; + extern const QString HA_INFORMATION; + extern const QString HA_COPYRIGHT; +} // namespace c + +namespace p /* for Parameters */ { + extern QString WORKING_DIR; + extern QStringList IGNORED_DESTINATIONS; + extern QString SERVICE_TITLE; + extern uint LAST_AND_TOP_LIST_SIZE; + extern QString BIND_TO_ADDRESS; + extern quint16 BIND_TO_PORT; +} // namespace p + +extern std::list< std::pair > instanses; + +enum LogLevel : uint8_t +{ + Off = 0, + Error = 1, + Warning = 2, + Info = 3, + Debug = 4 +}; +extern LogLevel logLevel; + +void customMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg); +enum class GetValueType { // QString getValue() + Default = 1, + Url = 2, + HttpHeader = 3 +}; +QString getValue(const QString &string, const QString &key, GetValueType type = GetValueType::Default); +QString hash(const QByteArray &data); +QString hash(QFile file); + +} // namespace g diff --git a/src/html-static.qrc b/src/html-static.qrc new file mode 100644 index 0000000..6ee62f3 --- /dev/null +++ b/src/html-static.qrc @@ -0,0 +1,6 @@ + + + html/index.html + html/main.css + + diff --git a/src/html/icons/arrow_up.svg b/src/html/icons/arrow_up.svg new file mode 100644 index 0000000..79e7cde --- /dev/null +++ b/src/html/icons/arrow_up.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/html/index.html b/src/html/index.html new file mode 100644 index 0000000..5f463bc --- /dev/null +++ b/src/html/index.html @@ -0,0 +1,121 @@ + + + + + + {{PAGE_TITLE}} + {{CUSTOM_CSS}} + + +
+
+

+{{PAGE_TITLE}} +

+
+
+ Last destinations +
+
    +{{LAST_DESTINATIONS_LIST}} +
+
+
+
+ Traffic +
+
+
+
+ Daily upload +
+
+
+ + + + + + + +
+
+ {{DAILY_UPLOAD}} +
+
+ {{DAILY_UPLOAD_MEASURE}} +
+
+
+
+ Total +
+
+ {{TOTAL_UPLOAD}} +
+
+
+
+
+ Daily download +
+
+
+ + + + + +
+
+ {{DAILY_DOWNLOAD}} +
+
+ {{DAILY_DOWNLOAD_MEASURE}} +
+
+
+
+ Total +
+
+ {{TOTAL_DOWNLOAD}} +
+
+
+
+
+
+
+ Top destinations +
+
+
+ Daily +
+
    +{{DAILY_TOP_DESTINATIONS_LIST}} +
+
+
+
+ Total +
+
    +{{TOTAL_TOP_DESTINATIONS_LIST}} +
+
+
{{BLOCKED_DESTINATIONS}}{{INFORMATION}} +
+
+ + + diff --git a/src/html/main.css b/src/html/main.css new file mode 100644 index 0000000..1ef2085 --- /dev/null +++ b/src/html/main.css @@ -0,0 +1 @@ +*{box-sizing:border-box;outline:none;z-index:1}*:hover{outline:none}*:focus{outline:none}html{width:100%;font-size:16px;scroll-behavior:smooth}body{overflow-y:scroll;height:100%;line-height:1.42;margin:0;background-color:#fff;color:#000;height:100vh;font-family:monospace;font-size:16px}main{display:flex;flex-flow:column nowrap;align-items:stretch;justify-content:flex-start}ul,ol{padding:0;margin:0}li{list-style:none}a{text-decoration:none;color:inherit}a:hover{text-decoration:none}button{cursor:pointer;background-color:rgba(0,0,0,0);border:0px solid rgba(0,0,0,0);padding:0}button:focus,input:focus,a:focus,textarea:focus{text-decoration:none}button,input,textarea{font-family:inherit;font-size:inherit}textarea{resize:none}p{margin:0}h1,h2,h3,h4,h5{margin-top:0;margin-bottom:0;font-weight:normal;font-size:100%}img{display:block;max-width:100%;height:auto}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}table{border-collapse:collapse}input[type=checkbox]{margin:0}.wrapper{height:100%;padding:16px 0;flex:1}.container{max-width:648px;margin:0 auto;padding:0 5px}.section{margin-bottom:35px}.section__title{margin-bottom:10px;font-size:20px;font-weight:bold}.proxyService__title{text-align:center;font-size:50px;padding:12px 0;font-weight:bold}.handledTraffic{width:100%}.handledTraffic__blocks{width:100%;display:flex;flex-flow:row nowrap;align-items:start;justify-content:center;gap:10px}.handledTraffic__block{width:calc(50% - 5px)}.handledTraffic__block_download .handledTraffic__svg{fill:#91fd91}.handledTraffic__block_upload .handledTraffic__svg{fill:#ff6f6f}.handledTraffic__title{text-transform:uppercase;font-weight:bold;background-color:#ddd;padding:3px 5px;font-size:20px}.handledTraffic__amount{display:flex;flex-flow:row nowrap;align-items:stretch;justify-content:center;background-color:#aaa;padding:10px;border-top:1px solid #000;border-bottom:1px solid #000}.handledTraffic__icon{text-align:center;display:flex;flex-flow:column nowrap;align-items:center;justify-content:center;padding:3px 8px;margin-right:5px}.handledTraffic__icon .handledTraffic__svg{height:35px}.handledTraffic__number{line-height:1;font-size:56px;font-weight:bold;text-transform:uppercase;margin-right:8px}.handledTraffic__measure{font-size:18px;font-weight:bold;align-self:end}.handledTraffic__total{background-color:#ddd;padding:8px;font-size:16px;text-align:right}.handledTraffic__totalTitle{text-transform:uppercase;font-weight:bold}.accordeo__radio{display:none}.accordeo__radio:checked~.accordeo__dropdown{display:block}.accordeo__dropdown{display:none}.blockedDestinations__item{background-color:#ddd;overflow:hidden;white-space:nowrap;margin-bottom:5px;text-overflow:ellipsis}.blockedDestinations__url{display:block;width:100%;padding:3px 6px;cursor:pointer}.blockedDestinations__ipList{background-color:#aaa;padding-left:10px}.blockedDestinations__ipItem{padding:4px}.lastDestinations{width:100%;padding:5px 0}.lastDestinations__list{width:100%}.lastDestinations__item{width:100%;margin-bottom:5px;padding:3px 6px;overflow:hidden;text-overflow:ellipsis}.lastDestinations__item:last-child{margin-bottom:0}.lastDestinations__item_blocked{background-color:#888}.lastDestinations__item_allowed{background-color:#ddd}.topDestinations__block{margin-bottom:14px}.topDestinations__block:last-child{margin-bottom:0}.topDestinations__title{font-weight:bold;margin-bottom:10px}.topDestinations__item{padding:4px;overflow:hidden;white-space:nowrap;margin-bottom:5px;text-overflow:ellipsis;background-color:#ddd}.copyright{padding:10px 0;text-align:center;color:#888} diff --git a/src/httpdocument.cpp b/src/httpdocument.cpp new file mode 100644 index 0000000..6def323 --- /dev/null +++ b/src/httpdocument.cpp @@ -0,0 +1,177 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "httpdocument.h" +#include "g.h" + +#include +#include +#include + +const QString HttpDocument::Code::_200 = "200 OK"; +const QString HttpDocument::Code::_303 = "303 See Other"; +const QString HttpDocument::Code::_304 = "304 Not Modified"; +const QString HttpDocument::Code::_400 = "400 Bad request"; +const QString HttpDocument::Code::_403 = "403 Access Denied"; +const QString HttpDocument::Code::_404 = "404 Not Found"; +const QString HttpDocument::Code::_413 = "413 Payload Too Large"; +const QString HttpDocument::Code::_500 = "500 Internal Error"; + +const QString HttpDocument::ContentType::HTML = "text/html; charset=utf-8"; +const QString HttpDocument::ContentType::TEXT = "text/plain; charset=utf-8"; +const QString HttpDocument::ContentType::CSS = "text/css"; +const QString HttpDocument::ContentType::SVG = "image/svg+xml"; +const QString HttpDocument::ContentType::ICO = "image/ico"; +const QString HttpDocument::ContentType::PNG = "image/png"; +const QString HttpDocument::ContentType::GIF = "image/gif"; +const QString HttpDocument::ContentType::JS = "text/javascript; charset=utf-8"; +const QString HttpDocument::ContentType::WOFF = "font/woff"; +const QString HttpDocument::ContentType::WOFF2 = "font/woff2"; +const QString HttpDocument::ContentType::JSON = "application/json; charset=utf-8"; +const QString HttpDocument::ContentType::MP3 = "audio/mpeg"; +const QString HttpDocument::ContentType::BIN = "application/octet-stream"; + +const QString HttpDocument::CommonHeader::ContentType = "Content-Type"; +const QString HttpDocument::CommonHeader::ContentLength = "Content-Length"; +const QString HttpDocument::CommonHeader::ETag = "ETag"; +const QString HttpDocument::CommonHeader::ProcessingDuration = "Processing-duration"; + +HttpDocument::HttpDocument() : + m_processingDurationStartMarker(QDateTime::currentMSecsSinceEpoch()) +{ + setCode(Code::_200); +} + +void HttpDocument::setCode(const QString &code) +{ + m_code = code; +} + +void HttpDocument::setHeader(const QString &name, const QString &value) +{ + if (name.isEmpty() or value.isEmpty()) return; + + m_headers.insert(name, value); +} + +void HttpDocument::setBody(const QByteArray &body) +{ + m_body = body; +} + +void HttpDocument::setBody(const QJsonObject &body) +{ + m_body = QJsonDocument(body).toJson(QJsonDocument::JsonFormat::Compact); + setHeader(CommonHeader::ContentType, ContentType::JSON); +} + +void HttpDocument::setBody(QFile payload, const QString& oldETag) +{ + if (not payload.exists()) + { + qDebug() << "File" << payload.fileName() << "not found"; + + setCode(Code::_404); + setBodySingleHTMLLabel("Not found", "Not found"); + return; + } + + if (not payload.open(QIODevice::ReadOnly)) + { + qDebug() << "File" << payload.fileName() << "reading failed"; + + setCode(Code::_500); + setBodySingleHTMLLabel("Not available", "Can't read"); + return; + } + + QByteArray fileBytes = payload.readAll(); + payload.close(); + + QString eTag = g::hash(fileBytes); + + if (oldETag == eTag) + { + qDebug() << "File cached by user:" << payload.fileName() << "/ Hash:" << oldETag; + + setCode(Code::_304); + return; + } + + setBody(fileBytes); + setHeader(CommonHeader::ETag, eTag); + + setHeader(CommonHeader::ContentType, + payload.fileName().endsWith(".html") ? ContentType::HTML : + payload.fileName().endsWith(".css") ? ContentType::CSS : + payload.fileName().endsWith(".js") ? ContentType::JS : + payload.fileName().endsWith(".ico") ? ContentType::ICO : + payload.fileName().endsWith(".svg") ? ContentType::SVG : + payload.fileName().endsWith(".png") ? ContentType::PNG : + payload.fileName().endsWith(".woff") ? ContentType::WOFF : + payload.fileName().endsWith(".woff2") ? ContentType::WOFF2 : + payload.fileName().endsWith(".json") ? ContentType::JSON : + payload.fileName().endsWith(".mp3") ? ContentType::MP3 : + payload.fileName().endsWith(".gif") ? ContentType::GIF : + payload.fileName().endsWith(".txt") ? ContentType::TEXT : + ContentType::BIN); +} + +void HttpDocument::setBodySingleHTMLLabel(const QString &text, const QString& title) +{ + m_body = "\r\n" + "\r\n" + " "+title.toUtf8()+"\r\n" + "\r\n" + "\r\n" + "
\r\n" + "

"+text.toUtf8()+"

\r\n" + "

[back to main]

\r\n" + "

\r\n" + "

\r\n" + "

3PROXY-EAGLE

\r\n" + "
"; +} + +QByteArray HttpDocument::document() const +{ + static constexpr const char HTML_NEWLINE[] {"\r\n"}; + + QByteArray result = "HTTP/1.0 " + code().toUtf8() + HTML_NEWLINE; + + auto h = headers(); + QMapIterator iter(h); + while (iter.hasNext()) + { + iter.next(); + result += (iter.key()+": "+iter.value()+HTML_NEWLINE).toUtf8(); + } + + result += QString( CommonHeader::ContentLength + ": " + + QString::number( body().size()).toUtf8() ).toUtf8() + HTML_NEWLINE; + result += QString( CommonHeader::ProcessingDuration + ": " + + QString::number( QDateTime::currentMSecsSinceEpoch()-m_processingDurationStartMarker) ).toUtf8() + HTML_NEWLINE; + result += HTML_NEWLINE; + + result += body(); + + return result; +} diff --git a/src/httpdocument.h b/src/httpdocument.h new file mode 100644 index 0000000..c9b5317 --- /dev/null +++ b/src/httpdocument.h @@ -0,0 +1,87 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include +#include +#include +#include + +class HttpDocument +{ +public: + struct CommonHeader + { + static const QString ContentType; + static const QString ContentLength; + static const QString ETag; + static const QString ProcessingDuration; + }; + + struct ContentType + { + static const QString HTML; + static const QString TEXT; + static const QString CSS; + static const QString SVG; + static const QString ICO; + static const QString PNG; + static const QString GIF; + static const QString JS; + static const QString WOFF; + static const QString WOFF2; + static const QString JSON; + static const QString MP3; + static const QString BIN; + }; + + struct Code { + static const QString _200; + static const QString _303; + static const QString _304; + static const QString _400; + static const QString _403; + static const QString _404; + static const QString _413; + static const QString _500; + }; + + HttpDocument(); + void setCode(const QString& code = Code::_200); + QString code() const { return m_code; } + + void setHeader(const QString& name, const QString& value); + QMap headers() const { return m_headers; } + + void setBody(const QByteArray& body); + void setBody(const QJsonObject& body); + void setBody(QFile payload, const QString& eTag = QString()); + void setBodySingleHTMLLabel(const QString& text, const QString& title = "Notice"); + QByteArray body() const { return m_body; } + + QByteArray document() const; + +private: + QString m_code = Code::_200; + QMap m_headers; + QByteArray m_body; + + const qint64 m_processingDurationStartMarker; +}; diff --git a/src/httpserver.cpp b/src/httpserver.cpp new file mode 100644 index 0000000..6d1f148 --- /dev/null +++ b/src/httpserver.cpp @@ -0,0 +1,43 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "httpserver.h" +#include "socketrunnable.h" + +HttpServer::HttpServer(QObject *parent) : + WebServerBase(parent) +{ + +} + +void HttpServer::killTheCapitalism() +{ + qWarning() << "Try to kill capitalism..."; + QThread::sleep(1); + qWarning() << "Pick up black flag..."; + QThread::sleep(1); + qWarning() << "Hmmm. Timed out. Try later with friends!"; +} + +void HttpServer::incomingConnection(qintptr handle) +{ + SocketRunnable* socket = new SocketRunnable(handle); + socket->setAutoDelete(true); + pool()->start(socket); +} diff --git a/src/httpserver.h b/src/httpserver.h new file mode 100644 index 0000000..17630e8 --- /dev/null +++ b/src/httpserver.h @@ -0,0 +1,33 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "webserverbase.h" + +class HttpServer : public WebServerBase +{ +public: + HttpServer(QObject *parent = nullptr); + void killTheCapitalism(); + + // QTcpServer interface +protected: + void incomingConnection(qintptr handle); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4b06ded --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,209 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "proxyinstanse.h" +#include "httpserver.h" +#include "g.h" + +#include +#include +#include +#include +#include +#include + +void terminate(int) +{ + std::cout << "Terminating..." << std::endl; + for (const auto& instanse: g::instanses) + { + instanse.first->terminate(); + } +} + +void usage() +{ + std::cout << g::c::SOFTWARE_NAME.toStdString() << " " << g::c::SOFTWARE_VERSION.toStdString() << std::endl + << "Accumulate ethical 3proxy statistics with web interface\n\n" + + "U S A G E:\n" + " -i --instanse <3proxy>,<3proxy.cfg>\n" + " -w --working-directory \n" + " -t --service-title <3proxy-eagle>\n" + " -s --last-and-top-list-size <5>\n" + " -I --ignored-destinations <[0.0.0.0],127.0.0.1>\n" + " -a --bind-to-address <127.0.0.1>\n" + " -p --bind-to-port <8161>\n" + " -l --log-level (off, error, warn, info, debug)\n\n" + + "N O T E S:\n" + "* Multi instanses supported. Just pass new one --instanse value!\n" + "* Main 3proxy cfg must contain log to stdout with strict format:\n" + " log\n" + " logformat \" type=%N destination=%n to=%I from=%O\"\n" + "* 3proxy cfg can contain blocked domains in format:\n" + " {{vk.com,mail.ru,google.com}}\n" + " This domains will be resolved automatically and replaced by:\n" + " deny * * original.domain\n" + " deny * * 8.8.8.8,1.1.1.1 # resolved addresses" + "* If the working directory contains a information.html, the Information\n" + " box will be added to the web page. The block is full html-formatted.\n" + "* The html folder can contain any files, they will be available\n" + " for downloading through the web browser.\n" + "* html folder can contain styles.css file for overloading default styles.\n" + " See page source via web browser to customize CSS classes.\n" + + "\n" << g::c::COPYRIGHT.toStdString() << std::endl; +} + +int main(int argc, char *argv[]) +{ + signal(SIGINT, terminate); + signal(SIGTERM, terminate); + + qInstallMessageHandler(g::customMessageOutput); + + QCoreApplication a(argc, argv); + + QList> instanses; + + for (int i = 1; i < argc; i++) + { + QString key(argv[i]); + QString value; + if (i+1 < argc) + { + value = argv[i+1]; + } + + if (key == "--log-level" and not value.isEmpty()) + { + if (value.contains("off", Qt::CaseInsensitive)) + { + g::logLevel = g::LogLevel::Off; + } + else if (value.contains("error", Qt::CaseInsensitive)) + { + g::logLevel = g::LogLevel::Error; + } + else if (value.contains("warn", Qt::CaseInsensitive)) + { + g::logLevel = g::LogLevel::Warning; + } + else if (value.contains("info", Qt::CaseInsensitive)) + { + g::logLevel = g::LogLevel::Info; + } + else if (value.contains("debug", Qt::CaseInsensitive)) + { + g::logLevel = g::LogLevel::Debug; + } + else + { + qWarning() << "Invalid log level flag:" << value << "(maybe you should read the --help)"; + } + } + + else if ((key == "-i" or key == "--instanse") and not value.isEmpty()) + { + QStringList splitted = value.split(','); + if (splitted.size() != 2) + { + qWarning() << "--instanse parsing failed:" << value; + continue; + } + instanses.push_back( {splitted.front(), splitted.back()} ); + } + + else if ((key == "-w" or key == "--working-directory") and not value.isEmpty()) + { + g::p::WORKING_DIR = value; + } + + else if ((key == "-s" or key == "--last-and-top-list-size") and not value.isEmpty()) + { + bool ok = false; + value.toUInt(&ok); + if (not ok) + { + qWarning() << "--last-and-top-list-size parsing failed, not a positive number:" << value; + continue; + } + g::p::LAST_AND_TOP_LIST_SIZE = value.toUInt(); + } + + else if ((key == "-I" or key == "--ignored-destinations") and not value.isEmpty()) + { + g::p::IGNORED_DESTINATIONS = value.split(','); + } + + else if ((key == "-b" or key == "--bind-to-address") and not value.isEmpty()) + { + g::p::BIND_TO_ADDRESS = value; + } + + else if ((key == "-p" or key == "--bind-to-port") and not value.isEmpty()) + { + bool ok = false; + value.toUShort(&ok); + if (not ok) + { + qWarning() << "--bind-to-port parsing failed, incorrect port value:" << value; + continue; + } + g::p::BIND_TO_PORT = value.toUShort(); + } + + else if ((key == "-t" or key == "--service-title") and not value.isEmpty()) + { + g::p::SERVICE_TITLE = value; + } + + else if (key == "-h" or key == "--help") + { + usage(); + return 0; + } + } + + qInfo().noquote() << "Instanses count:" << instanses.size(); + qInfo().noquote() << "Working directory:" << g::p::WORKING_DIR; + qInfo().noquote() << "Last and top dest list size:" << g::p::LAST_AND_TOP_LIST_SIZE; + qInfo().noquote() << "Ignored destinations:" << g::p::IGNORED_DESTINATIONS; + qInfo().noquote() << "Bind to address:" << g::p::BIND_TO_ADDRESS; + qInfo().noquote() << "Bind to port:" << g::p::BIND_TO_PORT; + qInfo() << ""; + + for (const auto& pair: instanses) + { + QThread* thread = new QThread; + ProxyInstanse* pi = new ProxyInstanse; + pi->set3proxyBinaryFile(pair.first); + pi->set3proxyConfigFile(pair.second); + pi->moveToThread(thread); + QObject::connect(thread, &QThread::started, pi, &ProxyInstanse::start); + QObject::connect(thread, &QThread::finished, pi, &ProxyInstanse::deleteLater); + thread->start(); + g::instanses.push_back( {thread, pi} ); + } + + (new HttpServer)/*->killTheCapitalism()*/; + + return a.exec(); +} diff --git a/src/proxyinstanse.cpp b/src/proxyinstanse.cpp new file mode 100644 index 0000000..2a27dac --- /dev/null +++ b/src/proxyinstanse.cpp @@ -0,0 +1,237 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "proxyinstanse.h" +#include "statistics.h" +#include "g.h" + +#include +#include +#include +#include + +ProxyInstanse::ProxyInstanse(QObject* parent) + : QObject(parent) +{ + +} + +ProxyInstanse::~ProxyInstanse() +{ + qInfo() << "Proxy instanse" << g::hash(confPath().toUtf8()) << "terminated"; + + if (m_process) + { + m_process->terminate(); + connect(m_process, QOverload::of(&QProcess::finished), + m_process, &QProcess::deleteLater); + } +} + +void ProxyInstanse::set3proxyBinaryFile(const QString &pathTo3proxyBinFile) +{ + if (m_process) + { + qWarning() << "ProxyInstanse::set3proxyBinaryFile() process already started"; + } + if (not QFile::exists(pathTo3proxyBinFile)) + { + qCritical() << "ProxyInstanse::set3proxyBinaryFile()" << pathTo3proxyBinFile << "not exists"; + } + m_binPath = pathTo3proxyBinFile; +} + +void ProxyInstanse::set3proxyConfigFile(const QString &pathTo3proxyConfFile) +{ + if (m_process) + { + qWarning() << "ProxyInstanse::set3proxyConfigFile() process already started"; + } + + if (not QFile::exists(pathTo3proxyConfFile)) + { + qCritical() << "ProxyInstanse::set3proxyConfigFile()" << pathTo3proxyConfFile << "not exists"; + } + + m_confPath = pathTo3proxyConfFile; + + createConfigurationFile(); +} + +void ProxyInstanse::generateBlackList(const QStringList &domainsOrAddresses) +{ + qDebug() << "ProxyInstanse::generateBlackList() raw:" << domainsOrAddresses; + + QStringListIterator iterRaw(domainsOrAddresses); + while (iterRaw.hasNext()) + { + QString name = iterRaw.next(); + m_blackList.insert(name, QStringList()); + static QRegularExpression rgx_hiddenNetworks("^.*\\.(i2p|onion|loki)$"); + if (name.contains(rgx_hiddenNetworks)) + { + qDebug() << name << "is hidden network name, no attempt to resolve"; + continue; + } + + QHostInfo host = QHostInfo::fromName(name); + if (host.error() != QHostInfo::NoError) + { + qDebug() << "Can not resolve" << name << ":" << host.errorString(); + continue; + } + else if (host.addresses().first().toString() == name) + { + qDebug() << name << "not a domain name (it's normal, just debug info)"; + continue; + } + + auto resolved = host.addresses(); + for (auto it = resolved.begin(); it != resolved.end(); ++it) + { + QString address = it->toString(); + qDebug() << "Domain" << name << "resolved to" << address; + m_blackList[name].push_back(address); + } + } +} + +QString ProxyInstanse::workingConfigPath() const +{ + return g::p::WORKING_DIR + "/" + g::hash(confPath()) + ".cfg"; +} + +void ProxyInstanse::start() +{ + m_process = new QProcess; + m_process->setProgram(binPath()); + m_process->setArguments(QStringList() << workingConfigPath()); + + connect (m_process, &QProcess::readyReadStandardOutput, this, &ProxyInstanse::reader); + connect (m_process, QOverload::of(&QProcess::finished), + this, &ProxyInstanse::restart); + + m_process->start(); +} + +void ProxyInstanse::reader() +{ + QMutexLocker lock (&m_readerMtx); + + m_readBuffer = m_process->readAll(); + + bool lastStringIsUncomplete = not m_readBuffer.endsWith('\n'); + QStringList completeLines = m_readBuffer.split('\n'); + + if (lastStringIsUncomplete) + { + m_readBuffer = completeLines.last(); + completeLines.pop_back(); + } + + for (const auto& str: completeLines) + { + if (str.isEmpty()) continue; + QString type = g::getValue(str, "type"); + QString dest = g::getValue(str, "destination"); + quint64 to = g::getValue(str, "to").toULongLong(); + quint64 from = g::getValue(str, "from").toULongLong(); + + if (type.isEmpty() or dest.isEmpty()) + { + qWarning() << "Receive incorrect log format. Please read --help for usage information"; + continue; + } + + qDebug() << "Type:" << type + << "dest:" << dest + << "to:" << to + << "from:" << from; + + Statistics::report( {type, dest, to, from} ); + } +} + +void ProxyInstanse::restart() +{ + qCritical() << "Called because process unexpected finished"; + m_process->deleteLater(); + start(); +} + +void ProxyInstanse::createConfigurationFile() +{ + QFile original (confPath()); + if (not original.open(QIODevice::ReadOnly)) + { + qFatal("Can not read original cfg"); + return; + } + QString originalFile = original.readAll(); + original.close(); + + static QRegularExpression rgx_domainsPattern ("^.*\\{\\{.*\\}\\}.*", QRegularExpression::DotMatchesEverythingOption); + if (originalFile.contains(rgx_domainsPattern)) + { + QString rawDomainsList = originalFile; + static QRegularExpression rgx_listBegin ("^.*\\{\\{", QRegularExpression::DotMatchesEverythingOption); + rawDomainsList.remove(rgx_listBegin); + static QRegularExpression rgx_listEnd ("\\}\\}.*$", QRegularExpression::DotMatchesEverythingOption); + rawDomainsList.remove(rgx_listEnd); + if (rawDomainsList.isEmpty()) + { + qWarning() << "Blocked domains pattern detected, but list is empty in" << confPath(); + } + else + { + generateBlackList(rawDomainsList.split(',')); + + QString blockedDomainsConfigPart; + QMapIterator mapIterator (m_blackList); + while (mapIterator.hasNext()) + { + auto currentNode = mapIterator.next(); + blockedDomainsConfigPart += "deny * * " + currentNode.key() + '\n'; + for (const auto& addr: currentNode.value()) + { + blockedDomainsConfigPart += "deny * * " + addr + '\n'; + } + } + + static QRegularExpression rgx_domains ("\\{\\{.*\\}\\}", QRegularExpression::DotMatchesEverythingOption); + originalFile.replace(rgx_domains, blockedDomainsConfigPart); + } + } + + QFile tmpConfig (workingConfigPath()); + if (not tmpConfig.open(QIODevice::WriteOnly)) + { + qFatal("Can not write working config"); + return; + } + + auto cfgByteArray = originalFile.toUtf8(); + int cfgSize = cfgByteArray.size(); + int writed = tmpConfig.write(cfgByteArray); + if (cfgSize != writed) + { + qCritical() << "Write error:" << writed << "/" << cfgSize; + } + tmpConfig.close(); +} diff --git a/src/proxyinstanse.h b/src/proxyinstanse.h new file mode 100644 index 0000000..c3711c2 --- /dev/null +++ b/src/proxyinstanse.h @@ -0,0 +1,59 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include +#include +#include +#include + +class ProxyInstanse : public QObject +{ + Q_OBJECT +public: + ProxyInstanse(QObject* parent = nullptr); + ~ProxyInstanse(); + void set3proxyBinaryFile(const QString& pathTo3proxyBinFile); + void set3proxyConfigFile(const QString& pathTo3proxyConfFile); + + QString binPath() const { return m_binPath; } + QString confPath() const { return m_confPath; } + QMap blackList() const { return m_blackList; } + +public slots: + void start(); + +private slots: + void reader(); + void restart(); + +private: + void createConfigurationFile(); + void generateBlackList(const QStringList& domainsOrAddresses); + QString workingConfigPath() const; + + QString m_binPath; + QString m_confPath; + QProcess* m_process = nullptr; + QMap m_blackList; // domain, addresses + + QMutex m_readerMtx; + QString m_readBuffer; +}; diff --git a/src/socketrunnable.cpp b/src/socketrunnable.cpp new file mode 100644 index 0000000..7ebb1d0 --- /dev/null +++ b/src/socketrunnable.cpp @@ -0,0 +1,118 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "socketrunnable.h" +#include "g.h" +#include "webpage.h" + +#include + +constexpr const int getValueBeforeSpaceLIMIT = 200; +constexpr const int getValueBeforeBodyLIMIT = 1000; + +SocketRunnable::SocketRunnable(qintptr socketDescriptor, QObject *parent) : + SocketRunnableBase(socketDescriptor, parent) +{ +} + +void SocketRunnable::get() +{ + if (urlPath() == "/index.html") + { + httpDocument().setBody(WebPage::document()); + return; + } + + QString eTag = g::getValue(m_headers, "If-None-Match", g::GetValueType::HttpHeader); + + if (urlPath() == "/main.css") + { + httpDocument().setBody(QFile(":/html/main.css"), eTag); + } + else + { + httpDocument().setBody(QFile(g::p::WORKING_DIR + "/html" + urlPath()), eTag); + } +} + +void SocketRunnable::reader() +{ + QString reqType = getValueBeforeSpace(); + m_urlPath = QByteArray::fromPercentEncoding( getValueBeforeSpace().toUtf8() ); + if (m_urlPath.contains("..")) + { + // Possible attempt to explore server directories + httpDocument().setCode(HttpDocument::Code::_404); + httpDocument().setBodySingleHTMLLabel("What you where looking for is not here", "Not found"); + return; + } + + if (m_urlPath == "/") + { + m_urlPath = "/index.html"; + } + m_headers = getValueBeforeBody(); + + if (reqType == "GET") + { + get(); + } + else if (reqType != "HEAD") + { + httpDocument().setCode(HttpDocument::Code::_400); // default code is 200 + } + + setDataToWrite(httpDocument().document()); +} + +QString SocketRunnable::getValueBeforeSpace() +{ + QString result; + char symbol {0}; + qint64 tail = socket()->read(&symbol, 1); + while (symbol != ' ' and tail > 0) + { + if (result.size() > getValueBeforeSpaceLIMIT) + { + qDebug() << "Parsing error: TYPE or URL length >" << getValueBeforeSpaceLIMIT; + break; + } + result += symbol; + tail = socket()->read(&symbol, 1); + } + return result; +} + +QString SocketRunnable::getValueBeforeBody() +{ + QString result; + char symbol {0}; + qint64 tail = socket()->read(&symbol, 1); + while (not result.endsWith("\r\n\r") and tail > 0) + { + if (result.size() > getValueBeforeBodyLIMIT) + { + qDebug() << "Parsing error: headers length >" << getValueBeforeBodyLIMIT; + break; + } + result += symbol; + tail = socket()->read(&symbol, 1); + } + return result; +} diff --git a/src/socketrunnable.h b/src/socketrunnable.h new file mode 100644 index 0000000..bf038e6 --- /dev/null +++ b/src/socketrunnable.h @@ -0,0 +1,49 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "socketrunnablebase.h" +#include "httpdocument.h" + +class SocketRunnable : public SocketRunnableBase +{ + Q_OBJECT +public: + SocketRunnable(qintptr, QObject* parent = nullptr); + + // SocketRunnableBase interface +public: + void reader() override; + +private: + HttpDocument& httpDocument() { return m_httpDocument; } + HttpDocument m_httpDocument; + + QString m_urlPath; + const QString& urlPath() const { return m_urlPath; } + + QString m_headers; + const QString& headers() const { return m_headers; } + + void get(); + + QString getValueBeforeSpace(); + QString getValueBeforeBody(); +}; diff --git a/src/socketrunnablebase.cpp b/src/socketrunnablebase.cpp new file mode 100644 index 0000000..7b61866 --- /dev/null +++ b/src/socketrunnablebase.cpp @@ -0,0 +1,82 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "socketrunnablebase.h" + +#include + +SocketRunnableBase::SocketRunnableBase(qintptr socketDescriptor, QObject *parent) : + QObject(parent), + m_socketDescriptor(socketDescriptor) +{ +} + +SocketRunnableBase::~SocketRunnableBase() +{ + if (m_socket) delete m_socket; +} + +void SocketRunnableBase::setDataToWrite(const QByteArray &data) +{ + m_data = data; +} + +QTcpSocket *SocketRunnableBase::socket() +{ + return m_socket; +} + +void SocketRunnableBase::run() +{ + if (not m_socketDescriptor) + { + return; + } + + m_socket = new QTcpSocket; + m_socket->setSocketDescriptor(m_socketDescriptor); + + if (m_socket->waitForReadyRead()) + { + reader(); + + if (not m_data.isEmpty()) + { + m_socket->write(m_data); + while (m_socket->bytesToWrite() > 0) + { + if (m_socket->state() == QTcpSocket::UnconnectedState) + { + qWarning() << "Socket turned to unconnected state at writing moment"; + break; + } + m_socket->waitForBytesWritten(100); + } + m_socket->close(); + } + else + { + qWarning() << "Data to write is empty"; + } + } + else + { + qWarning() << "Socket reading timed out"; + } +} diff --git a/src/socketrunnablebase.h b/src/socketrunnablebase.h new file mode 100644 index 0000000..d4105b5 --- /dev/null +++ b/src/socketrunnablebase.h @@ -0,0 +1,45 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "httpdocument.h" + +#include +#include +#include + +/* 1) override reader(); 2) call setDataToWrite() at and of reader() */ + +class SocketRunnableBase : public QObject, public QRunnable +{ + Q_OBJECT +public: + SocketRunnableBase(qintptr, QObject* parent = nullptr); + ~SocketRunnableBase(); + void run() override; + virtual void reader() = 0; + void setDataToWrite(const QByteArray& data); + QTcpSocket *socket(); + +private: + qintptr m_socketDescriptor; + QTcpSocket* m_socket = nullptr; + QByteArray m_data; +}; diff --git a/src/statistics.cpp b/src/statistics.cpp new file mode 100644 index 0000000..21c06a1 --- /dev/null +++ b/src/statistics.cpp @@ -0,0 +1,92 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "statistics.h" +#include "g.h" + +#include +#include +#include +#include +#include +#include + +QStringList Statistics::m_lastDestinations; +qint64 Statistics::m_lastSerializeTimestamp = 0; +bool Statistics::m_deserialized = false; +DailyCounter Statistics::m_dailyUpload; +DailyCounter Statistics::m_dailyDownload; +quint64 Statistics::m_totalUpload = 0; +quint64 Statistics::m_totalDownload = 0; + +void Statistics::report(const LogEvent& event) +{ + // mutex enabled in ProxyInstanse::reader() + + deserialize(); + + if (g::p::IGNORED_DESTINATIONS.contains(event.dest)) + { + qDebug() << "Destination" << event.dest << "ignored"; + return; + } + + addToDestinationsList(event.dest); + incrementUpload(event.to); + incrementDownload(event.from); + // type not handled + + serialize(); +} + +void Statistics::incrementUpload(quint64 value) +{ + m_dailyUpload.increment(value); + m_totalUpload += value; +} + +void Statistics::incrementDownload(quint64 value) +{ + m_dailyDownload.increment(value); + m_totalDownload += value; +} + +void Statistics::addToDestinationsList(const QString &dest) +{ + if (static_cast(lastDestinations().size()) >= g::p::LAST_AND_TOP_LIST_SIZE) + { + m_lastDestinations.pop_back(); + } + m_lastDestinations.push_front(dest); + + +} + +void Statistics::serialize() +{ + QFile file(g::p::WORKING_DIR + "/statistics.json"); +} + +void Statistics::deserialize() +{ + if (m_deserialized) return; + + QFile file(g::p::WORKING_DIR + "/statistics.json"); + +} diff --git a/src/statistics.h b/src/statistics.h new file mode 100644 index 0000000..a2e0695 --- /dev/null +++ b/src/statistics.h @@ -0,0 +1,64 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "dailycounter.h" + +#include + +struct LogEvent +{ + QString type; + QString dest; + quint64 to = 0; + quint64 from = 0; +}; + +class Statistics +{ +public: + Statistics() = delete; + static void report (const LogEvent& event); + + static const QStringList lastDestinations() { return QStringList{"google.com"}; /*m_lastDestinations;*/ } + static const QStringList dailyTopDestinations() { return QStringList{"cloudflare.com", "yahoo.com"}; } + static const QStringList totalTopDestinations() { return QStringList{"domain.com", "top.org"}; } + static quint64 dailyUpload() { return 124; } + static quint64 totalUpload() { return 241298412; } + static quint64 dailyDownload() { return 21312312312412; } + static quint64 totalDownload() { return 23112; } + +private: + static void incrementUpload(quint64 value); + static void incrementDownload(quint64 value); + static void addToDestinationsList(const QString& dest); + static void serialize(); + static void deserialize(); + + static QStringList m_lastDestinations; + + static qint64 m_lastSerializeTimestamp; + static bool m_deserialized; + static DailyCounter m_dailyUpload; + static DailyCounter m_dailyDownload; + static quint64 m_totalUpload; + static quint64 m_totalDownload; +}; + diff --git a/src/webpage.h b/src/webpage.h new file mode 100644 index 0000000..f317010 --- /dev/null +++ b/src/webpage.h @@ -0,0 +1,55 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include +#include + +class WebPage +{ +public: + WebPage() = delete; + static QByteArray document(); + +private: + static void setTitle(QString& document); + static void customStyles(QString& document); + static void lastDestinations(QString& document); + static void trafficBlock(QString& document); + static void topDestinations(QString& document); + static void blockedDestinations(QString& document); + static void informationBlock(QString& document); + static void copyright(QString& document); + + static QString lastDestinationItem(const QString& destination); + static QString dailyTopDestinationItem(const QString& destination); + static QString totalTopDestinationItem(const QString& destination); + static QString blockedDestinationItem(const QString& destination, const QStringList& addresses); + static QPair bytesToHumanReadableString(quint64 bytes); // count, measure + + static const QString m_lastDestinationItem; + static const QString m_dailyTopDestinationItem; + static const QString m_totalTopDestinationItem; + static const QString m_blockedDestinationsBlock; + static const QString m_blockedDestinationItemLvl1; + static const QString m_blockedDestinationItemLvl2; + static const QString m_informationBlock; +}; + diff --git a/src/webserverbase.cpp b/src/webserverbase.cpp new file mode 100644 index 0000000..23247d3 --- /dev/null +++ b/src/webserverbase.cpp @@ -0,0 +1,57 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "webserverbase.h" +#include "g.h" + +#include + +WebServerBase::WebServerBase(QObject *parent) : + QTcpServer(parent) +{ + m_pool = new QThreadPool(this); + auto threadCount = std::thread::hardware_concurrency() * 2; + m_pool->setMaxThreadCount(threadCount); + + if (not listen(QHostAddress(g::p::BIND_TO_ADDRESS), g::p::BIND_TO_PORT)) + { + throw std::runtime_error("Web server not binded at " + + g::p::BIND_TO_ADDRESS.toStdString() + " / " + + QString::number(g::p::BIND_TO_PORT).toStdString()); + } + qInfo() << "Web server started at" << g::p::BIND_TO_ADDRESS << "/" << g::p::BIND_TO_PORT; +} + +WebServerBase::~WebServerBase() +{ + if (m_pool) + { + m_pool->clear(); + if (not m_pool->waitForDone(100)) + { + qWarning() << "Thread pool of web server not finished at 100ms"; + } + m_pool->deleteLater(); + } +} + +QThreadPool *WebServerBase::pool() +{ + return m_pool; +} diff --git a/src/webserverbase.h b/src/webserverbase.h new file mode 100644 index 0000000..1f457c8 --- /dev/null +++ b/src/webserverbase.h @@ -0,0 +1,36 @@ +/* +3proxy-eagle: Accumulate ethical 3proxy statistics with web interface. +Source code: https://notabug.org/acetone/3proxy-eagle. +Copyright (C) 2022, acetone + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include +#include +#include + +class WebServerBase : public QTcpServer +{ + Q_OBJECT +public: + WebServerBase (QObject *parent = nullptr); + virtual ~WebServerBase(); + QThreadPool *pool(); + +private: + QThreadPool *m_pool; +};