Server now host the manual via its built in webserver., clients now by default display the server hosted manual, if not connectect the local version is opened.
Dieser Commit ist enthalten in:
Ursprung
771baaaf9d
Commit
ed5eeabd25
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -290,7 +290,7 @@ jobs:
|
||||
# Ubuntu only:
|
||||
- name: Install packages
|
||||
if: startswith(matrix.config.os, 'ubuntu')
|
||||
run: sudo apt install libboost-program-options-dev liblua5.4-dev lcov libarchive-dev clang-tidy libsystemd-dev
|
||||
run: sudo apt install libboost-program-options-dev libboost-url-dev liblua5.4-dev lcov libarchive-dev clang-tidy libsystemd-dev
|
||||
|
||||
# MacOS only:
|
||||
- name: Install brew packages
|
||||
|
||||
@ -511,13 +511,25 @@ MainWindow::MainWindow(QWidget* parent) :
|
||||
|
||||
menu = menuBar()->addMenu(Locale::tr("qtapp.mainmenu:help"));
|
||||
menu->addAction(Theme::getIcon("help"), Locale::tr("qtapp.mainmenu:help"),
|
||||
[]()
|
||||
[this]()
|
||||
{
|
||||
const auto manual = QString::fromStdString((getManualPath() / "en-us.html").string());
|
||||
if(QFile::exists(manual))
|
||||
if(m_connection)
|
||||
{
|
||||
QUrl url;
|
||||
url.setScheme("http");
|
||||
url.setHost(m_connection->peerAddress().toString());
|
||||
url.setPort(m_connection->peerPort());
|
||||
url.setPath("/manual/en/index.html");
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
else if(const auto manual = QString::fromStdString((getManualPath() / "en" / "index.html").string()); QFile::exists(manual))
|
||||
{
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(manual));
|
||||
}
|
||||
else
|
||||
{
|
||||
QDesktopServices::openUrl(QString("https://traintastic.org/manual?version=" TRAINTASTIC_VERSION_FULL));
|
||||
}
|
||||
})->setShortcut(QKeySequence::HelpContents);
|
||||
auto* subMenu = menu->addMenu(Locale::tr("qtapp.mainmenu:wizards"));
|
||||
subMenu->addAction(Locale::tr("wizard.introduction:title"), this, &MainWindow::showIntroductionWizard);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*
|
||||
* This file is part of the traintastic source code.
|
||||
*
|
||||
* Copyright (C) 2019-2024 Reinder Feenstra
|
||||
* Copyright (C) 2019-2025 Reinder Feenstra
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -172,6 +172,16 @@ QString Connection::errorString() const
|
||||
return m_socket->errorString();
|
||||
}
|
||||
|
||||
QHostAddress Connection::peerAddress() const
|
||||
{
|
||||
return m_socket->peerAddress();
|
||||
}
|
||||
|
||||
quint16 Connection::peerPort() const
|
||||
{
|
||||
return m_socket->peerPort();
|
||||
}
|
||||
|
||||
void Connection::connectToHost(const QUrl& url, const QString& username, const QString& password)
|
||||
{
|
||||
m_username = username;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*
|
||||
* This file is part of the traintastic source code.
|
||||
*
|
||||
* Copyright (C) 2019-2021,2023-2024 Reinder Feenstra
|
||||
* Copyright (C) 2019-2021,2023-2025 Reinder Feenstra
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -28,6 +28,7 @@
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
#include <QAbstractSocket>
|
||||
#include <QHostAddress>
|
||||
#include <QMap>
|
||||
#include <QUuid>
|
||||
#include <traintastic/network/message.hpp>
|
||||
@ -121,6 +122,9 @@ class Connection : public QObject, public std::enable_shared_from_this<Connectio
|
||||
SocketError error() const;
|
||||
QString errorString() const;
|
||||
|
||||
QHostAddress peerAddress() const;
|
||||
quint16 peerPort() const;
|
||||
|
||||
void connectToHost(const QUrl& url, const QString& username, const QString& password);
|
||||
void disconnectFromHost();
|
||||
|
||||
|
||||
@ -324,7 +324,7 @@ if(WIN32 AND NOT MSVC)
|
||||
endif()
|
||||
|
||||
# boost
|
||||
find_package(Boost 1.81 REQUIRED COMPONENTS program_options)
|
||||
find_package(Boost 1.81 REQUIRED COMPONENTS program_options url)
|
||||
target_include_directories(traintastic-server SYSTEM PRIVATE ${Boost_INCLUDE_DIRS})
|
||||
target_link_libraries(traintastic-server PRIVATE ${Boost_LIBRARIES})
|
||||
if(BUILD_TESTING)
|
||||
|
||||
@ -22,8 +22,12 @@
|
||||
|
||||
#include "server.hpp"
|
||||
#include <boost/beast/http/buffer_body.hpp>
|
||||
#include <boost/beast/http/file_body.hpp>
|
||||
#include <boost/url/url_view.hpp>
|
||||
#include <boost/url/parse.hpp>
|
||||
#include <span>
|
||||
#include <traintastic/network/message.hpp>
|
||||
#include <traintastic/utils/standardpaths.hpp>
|
||||
#include <version.hpp>
|
||||
#include "clientconnection.hpp"
|
||||
#include "httpconnection.hpp"
|
||||
@ -31,7 +35,10 @@
|
||||
#include "../core/eventloop.hpp"
|
||||
#include "../log/log.hpp"
|
||||
#include "../log/logmessageexception.hpp"
|
||||
#include "../utils/endswith.hpp"
|
||||
#include "../utils/setthreadname.hpp"
|
||||
#include "../utils/startswith.hpp"
|
||||
#include "../utils/stripprefix.hpp"
|
||||
|
||||
//#define SERVE_FROM_FS // Development option, NOT for production!
|
||||
#ifdef SERVE_FROM_FS
|
||||
@ -60,6 +67,53 @@ static constexpr std::string_view contentTypeTextHtml{"text/html"};
|
||||
static constexpr std::string_view contentTypeTextCss{"text/css"};
|
||||
static constexpr std::string_view contentTypeTextJavaScript{"text/javascript"};
|
||||
static constexpr std::string_view contentTypeImageXIcon{"image/x-icon"};
|
||||
static constexpr std::string_view contentTypeImagePng{"image/png"};
|
||||
static constexpr std::string_view contentTypeApplicationGzip{"application/gzip"};
|
||||
static constexpr std::string_view contentTypeApplicationJson{"application/json"};
|
||||
static constexpr std::string_view contentTypeApplicationXml{"application/xml"};
|
||||
|
||||
static constexpr std::array<std::string_view, 7> manualAllowedExtensions{{
|
||||
".html",
|
||||
".css",
|
||||
".js",
|
||||
".json",
|
||||
".png",
|
||||
".xml",
|
||||
".xml.gz",
|
||||
}};
|
||||
|
||||
std::string_view getContentType(std::string_view filename)
|
||||
{
|
||||
if(endsWith(filename, ".html"))
|
||||
{
|
||||
return contentTypeTextHtml;
|
||||
}
|
||||
else if(endsWith(filename, ".png"))
|
||||
{
|
||||
return contentTypeImagePng;
|
||||
}
|
||||
else if(endsWith(filename, ".css"))
|
||||
{
|
||||
return contentTypeTextCss;
|
||||
}
|
||||
else if(endsWith(filename, ".js"))
|
||||
{
|
||||
return contentTypeTextJavaScript;
|
||||
}
|
||||
else if(endsWith(filename, ".json"))
|
||||
{
|
||||
return contentTypeApplicationJson;
|
||||
}
|
||||
else if(endsWith(filename, ".xml"))
|
||||
{
|
||||
return contentTypeApplicationXml;
|
||||
}
|
||||
else if(endsWith(filename, ".gz"))
|
||||
{
|
||||
return contentTypeApplicationGzip;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
http::message_generator notFound(const http::request<http::string_body>& request)
|
||||
{
|
||||
@ -155,6 +209,53 @@ http::message_generator textJavaScript(const http::request<http::string_body>& r
|
||||
return text(request, contentTypeTextJavaScript, body);
|
||||
}
|
||||
|
||||
http::message_generator serveFileFromFileSystem(const http::request<http::string_body>& request, std::string_view target, const std::filesystem::path& root, std::span<const std::string_view> allowedExtensions)
|
||||
{
|
||||
if(request.method() != http::verb::get && request.method() != http::verb::head)
|
||||
{
|
||||
return methodNotAllowed(request, {http::verb::get, http::verb::head});
|
||||
}
|
||||
|
||||
if(const auto url = boost::urls::parse_origin_form(target))
|
||||
{
|
||||
const std::filesystem::path path = std::filesystem::weakly_canonical(root / url->path().substr(1));
|
||||
|
||||
if(std::mismatch(path.begin(), path.end(), root.begin(), root.end()).second == root.end() && std::filesystem::exists(path))
|
||||
{
|
||||
const auto filename = path.string();
|
||||
|
||||
if(endsWith(filename, allowedExtensions))
|
||||
{
|
||||
http::file_body::value_type file;
|
||||
boost::system::error_code ec;
|
||||
|
||||
file.open(filename.c_str(), boost::beast::file_mode::scan, ec);
|
||||
if(!ec)
|
||||
{
|
||||
http::response<http::file_body> response{
|
||||
std::piecewise_construct,
|
||||
std::make_tuple(std::move(file)),
|
||||
std::make_tuple(http::status::ok, request.version())};
|
||||
|
||||
response.set(http::field::server, serverHeader);
|
||||
response.set(http::field::content_type, getContentType(filename));
|
||||
response.content_length(file.size());
|
||||
response.keep_alive(request.keep_alive());
|
||||
|
||||
if(request.method() == http::verb::head)
|
||||
{
|
||||
response.body().close(); // don’t send file contents
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notFound(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Server::Server(bool localhostOnly, uint16_t port, bool discoverable)
|
||||
@ -162,6 +263,7 @@ Server::Server(bool localhostOnly, uint16_t port, bool discoverable)
|
||||
, m_acceptor{m_ioContext}
|
||||
, m_socketUDP{m_ioContext}
|
||||
, m_localhostOnly{localhostOnly}
|
||||
, m_manualPath{getManualPath()}
|
||||
{
|
||||
assert(isEventLoopThread());
|
||||
|
||||
@ -355,6 +457,7 @@ http::message_generator Server::handleHTTPRequest(http::request<http::string_bod
|
||||
"<body>"
|
||||
"<h1>Traintastic <small>v" TRAINTASTIC_VERSION_FULL "</small></h1>"
|
||||
"<ul>"
|
||||
"<li><a href=\"/manual/en/index.html\">Manual</a></li>"
|
||||
"<li><a href=\"/throttle\">Web throttle</a></li>"
|
||||
"</ul>"
|
||||
"</body>"
|
||||
@ -399,6 +502,14 @@ http::message_generator Server::handleHTTPRequest(http::request<http::string_bod
|
||||
{
|
||||
return textPlain(request, TRAINTASTIC_VERSION_FULL);
|
||||
}
|
||||
if(startsWith(target, "/manual"))
|
||||
{
|
||||
return serveFileFromFileSystem(
|
||||
request,
|
||||
stripPrefix(target, "/manual"),
|
||||
m_manualPath,
|
||||
manualAllowedExtensions);
|
||||
}
|
||||
return notFound(request);
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <thread>
|
||||
#include <filesystem>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ip/udp.hpp>
|
||||
@ -51,6 +52,7 @@ class Server : public std::enable_shared_from_this<Server>
|
||||
boost::asio::ip::udp::endpoint m_remoteEndpoint;
|
||||
const bool m_localhostOnly;
|
||||
std::list<std::shared_ptr<WebSocketConnection>> m_connections;
|
||||
std::filesystem::path m_manualPath;
|
||||
|
||||
void doReceive();
|
||||
static std::unique_ptr<Message> processMessage(const Message& message);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*
|
||||
* This file is part of the traintastic source code.
|
||||
*
|
||||
* Copyright (C) 2022 Reinder Feenstra
|
||||
* Copyright (C) 2022,2025 Reinder Feenstra
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -30,4 +30,16 @@ constexpr bool endsWith(std::string_view sv, std::string_view suffix)
|
||||
return sv.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), sv.rbegin());
|
||||
}
|
||||
|
||||
constexpr bool endsWith(std::string_view sv, std::span<const std::string_view> suffixes)
|
||||
{
|
||||
for(auto suffix : suffixes)
|
||||
{
|
||||
if(endsWith(sv, suffix))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"boost-program-options",
|
||||
"boost-signals2",
|
||||
"boost-uuid",
|
||||
"boost-url",
|
||||
{
|
||||
"name": "libarchive",
|
||||
"default-features": false,
|
||||
|
||||
@ -21,11 +21,48 @@
|
||||
*/
|
||||
|
||||
#include "standardpaths.hpp"
|
||||
#include <optional>
|
||||
#ifdef WIN32
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(WIN32) && !(defined(__MINGW32__) || defined(__MINGW64__))
|
||||
|
||||
#define getEnvironmentVariableAsPath(name) getEnvironmentVariableAsPathImpl(L ## name)
|
||||
|
||||
std::optional<std::filesystem::path> getEnvironmentVariableAsPathImpl(const wchar_t* name)
|
||||
{
|
||||
wchar_t* value = nullptr;
|
||||
size_t valueLength = 0;
|
||||
if(_wdupenv_s(&value, &valueLength, name) == 0 && value && valueLength != 0)
|
||||
{
|
||||
std::filesystem::path path(value);
|
||||
free(value);
|
||||
return path;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#define getEnvironmentVariableAsPath(name) getEnvironmentVariableAsPathImpl(name)
|
||||
|
||||
std::optional<std::filesystem::path> getEnvironmentVariableAsPathImpl(const char* name)
|
||||
{
|
||||
if(const char* value = getenv(name))
|
||||
{
|
||||
return std::filesystem::path(value);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
static std::filesystem::path getKnownFolderPath(REFKNOWNFOLDERID rfid)
|
||||
{
|
||||
@ -52,20 +89,10 @@ std::filesystem::path getLocalAppDataPath()
|
||||
|
||||
std::filesystem::path getLocalePath()
|
||||
{
|
||||
#if defined(WIN32) && !(defined(__MINGW32__) || defined(__MINGW64__))
|
||||
wchar_t* path = nullptr;
|
||||
size_t pathLength = 0;
|
||||
if(_wdupenv_s(&path, &pathLength, L"TRAINTASTIC_LOCALE_PATH") == 0 && path && pathLength != 0)
|
||||
if(auto path = getEnvironmentVariableAsPath("TRAINTASTIC_LOCALE_PATH"))
|
||||
{
|
||||
std::filesystem::path p(path);
|
||||
free(path);
|
||||
return p;
|
||||
return *path;
|
||||
}
|
||||
#else
|
||||
if(const char* path = getenv("TRAINTASTIC_LOCALE_PATH"))
|
||||
return std::filesystem::path(path);
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
return getProgramDataPath() / "traintastic" / "translations";
|
||||
#elif defined(__linux__)
|
||||
@ -77,10 +104,16 @@ std::filesystem::path getLocalePath()
|
||||
|
||||
std::filesystem::path getManualPath()
|
||||
{
|
||||
if(auto path = getEnvironmentVariableAsPath("TRAINTASTIC_MANUAL_PATH"))
|
||||
{
|
||||
return *path;
|
||||
}
|
||||
#ifdef WIN32
|
||||
return getProgramDataPath() / "traintastic" / "manual";
|
||||
#elif defined(__linux__)
|
||||
return "/opt/traintastic/manual";
|
||||
#else
|
||||
return {};
|
||||
return std::filesystem::current_path() / "manual";
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Laden…
x
In neuem Issue referenzieren
Einen Benutzer sperren