Skip to content

Commit

Permalink
System info tab and Server Speed Tests (#303)
Browse files Browse the repository at this point in the history
- added a new "STATUS" page that displays information about the user's environment and hardware and a server speed tests;
- added new API-methods:
  - struct sysinfo() - returns information about the user's environment and hardware;     
  - bool testserverspeed(url, serverId) - puts nzb file to be downloaded by the target server.
  
- API-method "status" now has 3 extra fields:
  - TotalDiskSpaceLo - Total disk space on ‘DestDir’, in bytes. This field contains the low 32-bits of 64-bit value
  - TotalDiskSpaceHi - Total disk space on ‘DestDir’, in bytes. This field contains the high 32-bits of 64-bit value
  - TotalDiskSpaceMB - Total disk space on ‘DestDir’, in megabytes.`
- fixed NZB generator: the last segment was incorrect.
  
## Lib changes

- Boost.Asio - cross-platform library for network.
  • Loading branch information
dnzbk authored Jul 31, 2024
1 parent 3f71747 commit c5dce75
Show file tree
Hide file tree
Showing 59 changed files with 3,209 additions and 232 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ if(NOT BUILD_ONLY_TESTS)
${CMAKE_SOURCE_DIR}/daemon/queue
${CMAKE_SOURCE_DIR}/daemon/remote
${CMAKE_SOURCE_DIR}/daemon/util
${CMAKE_SOURCE_DIR}/daemon/system
${INCLUDES}
)
endif()
Expand Down
199 changes: 199 additions & 0 deletions daemon/connect/HttpClient.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* This file is part of nzbget. See <https://nzbget.com>.
*
* Copyright (C) 2024 Denis <denis@nzbget.com>
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/


#include "nzbget.h"

#include "HttpClient.h"
#include "Util.h"

namespace HttpClient
{
namespace asio = boost::asio;
using tcp = boost::asio::ip::tcp;

#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
namespace ssl = boost::asio::ssl;
#endif

HttpClient::HttpClient() noexcept(false)
: m_context{}
, m_resolver{ m_context }
{
#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
m_sslContext.set_default_verify_paths();
#endif
}

const std::string HttpClient::GetLocalIP() const
{
return m_localIP;
}

std::future<Response> HttpClient::GET(std::string host_)
{
return std::async(std::launch::async, [this, host = std::move(host_)]
{
auto endpoints = m_resolver.resolve(host, GetProtocol());
auto socket = GetSocket();

Connect(socket, endpoints, host);
Write(socket, "GET", host);

return MakeResponse(socket);
}
);
}

std::string HttpClient::GetProtocol() const
{
#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
return "https";
#else
return "http";
#endif
}

Response HttpClient::MakeResponse(Socket& socket)
{
Response response;

asio::streambuf buf;
response.statusCode = ReadStatusCode(socket, buf);
response.headers = ReadHeaders(socket, buf);
response.body = ReadBody(socket, buf);

return response;
}

void HttpClient::Connect(Socket& socket, const Endpoints& endpoints, const std::string& host)
{
asio::connect(socket.lowest_layer(), endpoints);
#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
DoHandshake(socket, host);
#endif
SaveLocalIP(socket);
}

void HttpClient::Write(Socket& socket, const std::string& method, const std::string& host)
{
asio::write(socket, asio::buffer(GetHeaders(method, host)));
}

std::string HttpClient::ReadBody(Socket& socket, boost::asio::streambuf& buf)
{
boost::system::error_code ec;
asio::read_until(socket, buf, "\0", ec);
if (ec)
{
throw std::runtime_error("Failed to read the response body: " + ec.message());
}

auto bodyBuf = buf.data();
std::string body(asio::buffers_begin(bodyBuf), asio::buffers_begin(bodyBuf) + bodyBuf.size());
Util::Trim(body);

return body;
}

Headers HttpClient::ReadHeaders(Socket& socket, boost::asio::streambuf& buf)
{
asio::read_until(socket, buf, "\r\n\r\n");

std::istream stream(&buf);

Headers headers;
std::string line;
while (std::getline(stream, line) && line != "\r")
{
size_t colonPos = line.find(":");
if (colonPos != std::string::npos)
{
std::string key = line.substr(0, colonPos);
std::string value = line.substr(colonPos + 1);
Util::Trim(value);
headers.emplace(std::move(key), std::move(value));
}
}

return headers;
}

unsigned HttpClient::ReadStatusCode(Socket& socket, boost::asio::streambuf& buf)
{
asio::read_until(socket, buf, "\r\n");

std::istream stream(&buf);

std::string httpVersion;
unsigned statusCode;

stream >> httpVersion;
stream >> statusCode;

if (!stream || httpVersion.find("HTTP/") == std::string::npos)
{
throw std::runtime_error("Invalid response");
}

return statusCode;
}

std::string HttpClient::GetHeaders(const std::string& method, const std::string& host) const
{
std::string headers = method + " / HTTP/1.1\r\n";
headers += "Host: " + host + "\r\n";
headers += std::string("User-Agent: nzbget/") + Util::VersionRevision() + "\r\n";
#ifndef DISABLE_GZIP
headers += "Accept-Encoding: gzip\r\n";
#endif
headers += "Accept: */*\r\n";
headers += "Connection: close\r\n\r\n";

return headers;
}

void HttpClient::SaveLocalIP(Socket& socket)
{
m_localIP = socket.lowest_layer().local_endpoint().address().to_string();
}

#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
Socket HttpClient::GetSocket()
{
return ssl::stream<tcp::socket>{ m_context, m_sslContext };
}

void HttpClient::DoHandshake(Socket& socket, const std::string& host)
{
if (!SSL_set_tlsext_host_name(socket.native_handle(), host.c_str()))
{
throw std::runtime_error("Failed to configure SNI TLS extension.");
}

socket.handshake(ssl::stream_base::handshake_type::client);
}
#else
Socket HttpClient::GetSocket()
{
return tcp::socket{ m_context };
}
#endif

}
82 changes: 82 additions & 0 deletions daemon/connect/HttpClient.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* This file is part of nzbget. See <https://nzbget.com>.
*
* Copyright (C) 2024 Denis <denis@nzbget.com>
*
* 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 2 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 <https://www.gnu.org/licenses/>.
*/


#ifndef HTTP_CLIENT_H
#define HTTP_CLIENT_H

#include <string>
#include <thread>
#include <boost/asio.hpp>

#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
#include <boost/asio/ssl.hpp>
#endif

namespace HttpClient
{
#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
using Socket = boost::asio::ssl::stream<boost::asio::ip::tcp::socket>;
#else
using Socket = boost::asio::ip::tcp::socket;
#endif

using Headers = std::unordered_map<std::string, std::string>;
using Endpoints = boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>;

struct Response
{
Headers headers;
std::string body;
unsigned statusCode;
};

class HttpClient final
{
public:
HttpClient() noexcept(false);
HttpClient(const HttpClient&) = delete;
HttpClient operator=(const HttpClient&) = delete;
~HttpClient() = default;

std::future<Response> GET(std::string host);
const std::string GetLocalIP() const;

private:
void Connect(Socket& socket, const Endpoints& endpoints, const std::string& host);
void Write(Socket& socket, const std::string& method, const std::string& host);
Response MakeResponse(Socket& socket);
std::string GetHeaders(const std::string& method, const std::string& host) const;
void SaveLocalIP(Socket& socket);
unsigned ReadStatusCode(Socket& socket, boost::asio::streambuf& buf);
Headers ReadHeaders(Socket& socket, boost::asio::streambuf& buf);
std::string ReadBody(Socket& socket, boost::asio::streambuf& buf);
Socket GetSocket();
std::string GetProtocol() const;
boost::asio::io_context m_context;
boost::asio::ip::tcp::resolver m_resolver;
std::string m_localIP;
#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
void DoHandshake(Socket& socket, const std::string& host);
boost::asio::ssl::context m_sslContext{ boost::asio::ssl::context::tlsv13_client };
#endif
};
}

#endif
Loading

0 comments on commit c5dce75

Please sign in to comment.