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:
Reinder Feenstra 2025-09-16 20:40:02 +02:00
Ursprung 771baaaf9d
Commit ed5eeabd25
10 geänderte Dateien mit 206 neuen und 21 gelöschten Zeilen

Datei anzeigen

@ -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

Datei anzeigen

@ -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);

Datei anzeigen

@ -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;

Datei anzeigen

@ -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();

Datei anzeigen

@ -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)

Datei anzeigen

@ -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(); // dont 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);
}

Datei anzeigen

@ -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);

Datei anzeigen

@ -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

Datei anzeigen

@ -5,6 +5,7 @@
"boost-program-options",
"boost-signals2",
"boost-uuid",
"boost-url",
{
"name": "libarchive",
"default-features": false,

Datei anzeigen

@ -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
}