From 00ccbf4fbd180e6193c71b67ed80f383a259a77e Mon Sep 17 00:00:00 2001 From: Carles Fernandez Date: Sat, 15 Aug 2026 09:09:10 +0200 Subject: [PATCH] Improve NTRIP client --- src/algorithms/PVT/adapters/rtklib_pvt.cc | 2 + .../PVT/gnuradio_blocks/rtklib_pvt_gs.cc | 132 ++++- .../PVT/gnuradio_blocks/rtklib_pvt_gs.h | 5 + src/algorithms/PVT/libs/ntrip_rtcm_client.cc | 30 +- src/algorithms/PVT/libs/rtklib_solver.h | 7 + src/algorithms/libs/rtklib/rtklib.h | 41 +- src/algorithms/libs/rtklib/rtklib_stream.cc | 475 +++++++++++++++--- src/algorithms/libs/rtklib/rtklib_tls.cc | 5 + src/algorithms/libs/rtklib/rtklib_tls.h | 2 + 9 files changed, 602 insertions(+), 97 deletions(-) diff --git a/src/algorithms/PVT/adapters/rtklib_pvt.cc b/src/algorithms/PVT/adapters/rtklib_pvt.cc index 1dc7dc675..14424b84f 100644 --- a/src/algorithms/PVT/adapters/rtklib_pvt.cc +++ b/src/algorithms/PVT/adapters/rtklib_pvt.cc @@ -89,6 +89,8 @@ public: Ntrip_Credential_Guard(const Ntrip_Credential_Guard&) = delete; Ntrip_Credential_Guard& operator=(const Ntrip_Credential_Guard&) = delete; + Ntrip_Credential_Guard(Ntrip_Credential_Guard&&) = delete; + Ntrip_Credential_Guard& operator=(Ntrip_Credential_Guard&&) = delete; private: Pvt_Conf* d_configuration; diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc index 3f3a20e12..ee181367a 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.cc @@ -128,6 +128,50 @@ namespace wht = boost; namespace wht = std; #endif +namespace +{ +/* console tag for the RTKLIB solution status; empty for single-point and + no-solution so that the default output is unchanged */ +std::string solution_status_tag(int solution_status) +{ + switch (solution_status) + { + case SOLQ_FIX: + return " [RTK FIXED]"; + case SOLQ_FLOAT: + return " [RTK FLOAT]"; + case SOLQ_SBAS: + return " [SBAS]"; + case SOLQ_DGPS: + return " [DGNSS]"; + case SOLQ_PPP: + return " [PPP]"; + default: + return ""; + } +} + + +const std::string& solution_status_color(int solution_status) +{ + switch (solution_status) + { + case SOLQ_FIX: + return TEXT_BOLD_GREEN; + case SOLQ_FLOAT: + case SOLQ_DGPS: + return TEXT_BOLD_YELLOW; + case SOLQ_SBAS: + return TEXT_BOLD_CYAN; + case SOLQ_PPP: + return TEXT_BOLD_MAGENTA; + default: + return TEXT_RESET; + } +} +} // namespace + + rtklib_pvt_gs_sptr rtklib_make_pvt_gs(uint32_t nchannels, const Pvt_Conf& conf_, const rtk_t& rtk, @@ -202,6 +246,7 @@ rtklib_pvt_gs::rtklib_pvt_gs(uint32_t nchannels, d_pvt_errors_counter(0), d_last_ntrip_client_state(-1), d_last_fixed_base_status(-1), + d_last_solution_status(SOLQ_NONE), d_dump(conf_.dump), d_dump_mat(conf_.dump_mat && conf_.dump), d_rinex_output_enabled(conf_.rinex_output_enabled), @@ -2433,6 +2478,84 @@ void rtklib_pvt_gs::report_fixed_base_status() } +int rtklib_pvt_gs::displayed_solution_status() const +{ + const int status = d_user_pvt_solver->pvt_sol.stat; + /* RTKLIB stamps SOLQ_PPP whenever the PPP filter runs, even on broadcast + orbits and clocks; only claim PPP when precise products are loaded */ + if (status == SOLQ_PPP && !d_user_pvt_solver->has_precise_ephemeris()) + { + return SOLQ_SINGLE; + } + return status; +} + + +void rtklib_pvt_gs::report_solution_status() +{ + const int status = displayed_solution_status(); + if (status == d_last_solution_status) + { + return; + } + const int previous_status = d_last_solution_status; + d_last_solution_status = status; + /* announce only transitions involving a differential, SBAS or PPP state; + plain single-point operation keeps the classic console output */ + if (solution_status_tag(previous_status).empty() && solution_status_tag(status).empty()) + { + return; + } + std::ostringstream report; + switch (status) + { + case SOLQ_FIX: + report << "RTK ambiguities fixed (AR ratio " << std::fixed << std::setprecision(1) + << d_user_pvt_solver->pvt_sol.ratio << ", threshold " + << d_user_pvt_solver->pvt_sol.thres << ")"; + break; + case SOLQ_FLOAT: + if (previous_status == SOLQ_FIX) + { + report << "RTK fix lost: float solution (AR ratio " << std::fixed + << std::setprecision(1) << d_user_pvt_solver->pvt_sol.ratio << ")"; + } + else + { + report << "RTK float solution"; + } + break; + case SOLQ_DGPS: + report << "RTK degraded to code differential (DGNSS)"; + break; + case SOLQ_SBAS: + report << "SBAS-corrected solution"; + break; + case SOLQ_PPP: + report << "PPP solution"; + break; + default: + /* fell back to single-point (or lost the solution) from a labeled state */ + if (previous_status == SOLQ_SBAS) + { + report << "SBAS corrections lost: single-point solution"; + } + else if (previous_status == SOLQ_PPP) + { + report << "PPP lost: single-point solution"; + } + else + { + report << "RTK lost: single-point solution"; + } + break; + } + const std::string& color = solution_status_tag(status).empty() ? TEXT_BOLD_YELLOW : solution_status_color(status); + std::cout << color << report.str() << TEXT_RESET << std::endl; + LOG(INFO) << "Solution status change: " << report.str(); +} + + int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_items, gr_vector_void_star& output_items __attribute__((unused))) { @@ -2845,6 +2968,7 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item if (flag_pvt_valid == true) { + report_solution_status(); // experimental VTL tests // send tracking command // const std::shared_ptr trk_cmd_test = std::make_shared(TrackingCmd()); @@ -2955,7 +3079,8 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item ss << " is Lat = " << d_user_pvt_solver->get_latitude() << " [deg], Long = " << d_user_pvt_solver->get_longitude() << " [deg], Height = " << d_user_pvt_solver->get_height() - << " [m], with GDOP = " << d_user_pvt_solver->get_gdop(); + << " [m], with GDOP = " << d_user_pvt_solver->get_gdop() + << solution_status_tag(displayed_solution_status()); std::cout << ss.str() << std::endl; d_end = std::chrono::system_clock::now(); std::chrono::duration elapsed_seconds = d_end - d_start; @@ -3054,13 +3179,16 @@ int rtklib_pvt_gs::work(int noutput_items, gr_vector_const_void_star& input_item std::cout.setf(std::ios::fixed, std::ios::floatfield); auto* facet = new boost::posix_time::time_facet("%Y-%b-%d %H:%M:%S.%f %z"); std::cout.imbue(std::locale(std::cout.getloc(), facet)); + const int solq = displayed_solution_status(); + const std::string solq_tag = solution_status_tag(solq); + const std::string solq_txt = solq_tag.empty() ? "" : solution_status_color(solq) + solq_tag; std::cout << TEXT_BOLD_GREEN << "Position at " << time_solution << UTC_solution_str << " using " << d_user_pvt_solver->get_num_valid_observations() << " observations is Lat = " << std::fixed << std::setprecision(6) << d_user_pvt_solver->get_latitude() << " [deg], Long = " << d_user_pvt_solver->get_longitude() << " [deg], Height = " - << std::fixed << std::setprecision(2) << d_user_pvt_solver->get_height() << std::setprecision(ss) << " [m]" << TEXT_RESET << std::endl; + << std::fixed << std::setprecision(2) << d_user_pvt_solver->get_height() << std::setprecision(ss) << " [m]" << solq_txt << TEXT_RESET << std::endl; DLOG(INFO) << "RX clock offset: " << d_user_pvt_solver->get_time_offset_s() << "[s]"; std::cout diff --git a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h index ba0aea5b1..ce72dd8dc 100644 --- a/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h +++ b/src/algorithms/PVT/gnuradio_blocks/rtklib_pvt_gs.h @@ -166,6 +166,10 @@ private: void report_fixed_base_status(); + void report_solution_status(); + + int displayed_solution_status() const; + std::map interpolate_observables(const std::map& observables_map_t0, const std::map& observables_map_t1, double rx_time_s); @@ -285,6 +289,7 @@ private: uint32_t d_pvt_errors_counter; int d_last_ntrip_client_state; int d_last_fixed_base_status; + int d_last_solution_status; bool d_dump; const bool d_dump_mat; diff --git a/src/algorithms/PVT/libs/ntrip_rtcm_client.cc b/src/algorithms/PVT/libs/ntrip_rtcm_client.cc index 60114d8f9..db02b7c8e 100644 --- a/src/algorithms/PVT/libs/ntrip_rtcm_client.cc +++ b/src/algorithms/PVT/libs/ntrip_rtcm_client.cc @@ -171,6 +171,8 @@ public: Stream_Owner(const Stream_Owner&) = delete; Stream_Owner& operator=(const Stream_Owner&) = delete; + Stream_Owner(Stream_Owner&&) = delete; + Stream_Owner& operator=(Stream_Owner&&) = delete; bool open(const std::string& path, const std::string& hostname, const std::string& username, const std::string& password) @@ -206,9 +208,8 @@ public: auto* ntrip = static_cast(d_stream.port); std::memset(ntrip->user, 0, sizeof(ntrip->user)); std::memset(ntrip->passwd, 0, sizeof(ntrip->passwd)); - // RTKLIB initializes the socket descriptor to zero and may - // leave a closed descriptor behind after a failed connection. - // Prevent strclose() from closing stdin or a reused descriptor. + // Keep unopened and disconnected transports explicitly invalid + // before handing ownership back to RTKLIB's stream cleanup. if (ntrip->tcp != nullptr && ntrip->tcp->svr.state < 1) { ntrip->tcp->svr.sock = -1; @@ -232,11 +233,16 @@ private: class Rtcm_Owner { public: + // The analyzer loses the unique_ptr member's ownership while following + // init_rtcm(), whose failure paths also release all partial allocations. + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) Rtcm_Owner() - : d_rtcm(new rtcm_t()), - d_initialized(init_rtcm(d_rtcm.get()) != 0) + : d_rtcm(new rtcm_t{}) { - std::memset(d_rtcm.get(), 0, sizeof(rtcm_t)); + // init_rtcm() populates pointers owned by the value-initialized + // control structure, so it must run after d_rtcm is constructed. + // NOLINTNEXTLINE(cppcoreguidelines-prefer-member-initializer) + d_initialized = init_rtcm(d_rtcm.get()) != 0; } ~Rtcm_Owner() @@ -249,6 +255,8 @@ public: Rtcm_Owner(const Rtcm_Owner&) = delete; Rtcm_Owner& operator=(const Rtcm_Owner&) = delete; + Rtcm_Owner(Rtcm_Owner&&) = delete; + Rtcm_Owner& operator=(Rtcm_Owner&&) = delete; bool initialized() const { @@ -356,6 +364,9 @@ void resolve_ipv4_hostname(const std::shared_ptr& state, } // namespace +// The layout follows mutex-protected state groups. Saving a few padding bytes +// in this single-instance class is not worth separating each lock from its data. +// NOLINTNEXTLINE(clang-analyzer-optin.performance.Padding) class Ntrip_Rtcm_Client::Impl { public: @@ -373,6 +384,11 @@ public: secure_clear(&d_config.password); } + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + Impl(Impl&&) = delete; + Impl& operator=(Impl&&) = delete; + bool start() { std::lock_guard lifecycle_lock(d_lifecycle_mutex); @@ -689,7 +705,7 @@ private: // shutdown wait. Keep the exceptional joinable // handle alive; its task owns no client state // and still reports through the shared result. - (void)resolver.release(); + (void)resolver.release(); // NOLINT(bugprone-unused-return-value) } } catch (...) diff --git a/src/algorithms/PVT/libs/rtklib_solver.h b/src/algorithms/PVT/libs/rtklib_solver.h index 92a513d2e..613060395 100644 --- a/src/algorithms/PVT/libs/rtklib_solver.h +++ b/src/algorithms/PVT/libs/rtklib_solver.h @@ -140,6 +140,13 @@ public: Rtklib_Fixed_Base_Status get_fixed_base_status() const; double get_fixed_base_age_s() const; std::size_t get_fixed_base_common_satellites() const; + + //! true only if precise ephemeris and clock products are loaded; used to + //! distinguish real PPP from PPP-mode processing with broadcast products + inline bool has_precise_ephemeris() const + { + return d_nav_data.ne > 0 && d_nav_data.nc > 0; + } void store_has_data(const Galileo_HAS_data& new_has_data); void clear_has_corrections(); void update_has_corrections(const std::map& obs_map); diff --git a/src/algorithms/libs/rtklib/rtklib.h b/src/algorithms/libs/rtklib/rtklib.h index 4c2ecd45b..4baebf4f8 100644 --- a/src/algorithms/libs/rtklib/rtklib.h +++ b/src/algorithms/libs/rtklib/rtklib.h @@ -1203,25 +1203,28 @@ typedef struct typedef struct -{ /* ntrip control type */ - int state; /* state (0:close,1:wait,2:connect) */ - int type; /* type (0:server,1:client) */ - int version; /* NTRIP client version (1 or 2) */ - int tls_enabled; /* TLS transport enabled */ - int chunked; /* HTTP chunked transfer encoding */ - int chunk_state; /* chunk parser state (0:size,1:data,2:crlf) */ - unsigned int chunk_remaining; /* bytes left in current HTTP chunk */ - int nb; /* response buffer size */ - char url[256]; /* url for proxy */ - char host[256]; /* caster hostname (not proxy hostname) */ - char port[16]; /* caster port */ - char mntpnt[256]; /* mountpoint */ - char user[256]; /* user */ - char passwd[256]; /* password */ - char str[NTRIP_MAXSTR]; /* mountpoint string for server */ - unsigned char buff[NTRIP_MAXRSP]; /* response buffer */ - tcpcli_t *tcp; /* tcp client */ - void *tls_ctx; /* opaque Rtklib_Tls_Client pointer */ +{ /* ntrip control type */ + int state; /* state (0:close,1:wait,2:connect) */ + int type; /* type (0:server,1:client) */ + int version; /* NTRIP client version (1 or 2) */ + int tls_enabled; /* TLS transport enabled */ + int chunked; /* HTTP chunked transfer encoding */ + int chunk_state; /* chunk parser state (0:size,1:data,2:crlf) */ + unsigned int chunk_remaining; /* bytes left in current HTTP chunk */ + int nb; /* response buffer size */ + int request_length; /* buffered NTRIP request size */ + int request_offset; /* bytes of buffered request already sent */ + char url[256]; /* url for proxy */ + char host[256]; /* caster hostname (not proxy hostname) */ + char port[16]; /* caster port */ + char mntpnt[256]; /* mountpoint */ + char user[256]; /* user */ + char passwd[256]; /* password */ + char str[NTRIP_MAXSTR]; /* mountpoint string for server */ + unsigned char buff[NTRIP_MAXRSP]; /* response buffer */ + unsigned char request_buff[NTRIP_MAXRSP]; /* persistent non-blocking request buffer */ + tcpcli_t *tcp; /* tcp client */ + void *tls_ctx; /* opaque Rtklib_Tls_Client pointer */ } ntrip_t; diff --git a/src/algorithms/libs/rtklib/rtklib_stream.cc b/src/algorithms/libs/rtklib/rtklib_stream.cc index 6bffabf0a..0a4fe8532 100644 --- a/src/algorithms/libs/rtklib/rtklib_stream.cc +++ b/src/algorithms/libs/rtklib/rtklib_stream.cc @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -983,6 +984,7 @@ int gentcp(tcp_t *tcp, int type, char *msg) } if (!setsock(tcp->sock, msg)) { + tcp->sock = -1; tcp->state = -1; return 0; } @@ -1002,6 +1004,7 @@ int gentcp(tcp_t *tcp, int type, char *msg) std::snprintf(msg, MAXSTRMSG, "bind error (%d) : %d", errsock(), tcp->port); tracet(1, "gentcp: bind error port=%d err=%d\n", tcp->port, errsock()); closesocket(tcp->sock); + tcp->sock = -1; tcp->state = -1; return 0; } @@ -1015,6 +1018,7 @@ int gentcp(tcp_t *tcp, int type, char *msg) std::snprintf(msg, MAXSTRMSG, "address error (%s)", tcp->saddr); tracet(1, "gentcp: gethostbyname error addr=%s err=%d\n", tcp->saddr, errsock()); closesocket(tcp->sock); + tcp->sock = -1; tcp->state = 0; tcp->tcon = ticonnect; tcp->tdis = tickget(); @@ -1036,7 +1040,11 @@ int gentcp(tcp_t *tcp, int type, char *msg) void discontcp(tcp_t *tcp, int tcon) { tracet(3, "discontcp: sock=%d tcon=%d\n", tcp->sock, tcon); - closesocket(tcp->sock); + if (tcp->sock >= 0) + { + closesocket(tcp->sock); + tcp->sock = -1; + } tcp->state = 0; tcp->tcon = tcon; tcp->tdis = tickget(); @@ -1322,6 +1330,7 @@ int consock(tcpcli_t *tcpcli, char *msg) std::snprintf(msg, MAXSTRMSG, "connect error (%d)", err); tracet(1, "consock: connect error sock=%d err=%d\n", tcpcli->svr.sock, err); closesocket(tcpcli->svr.sock); + tcpcli->svr.sock = -1; tcpcli->svr.state = 0; return 0; } @@ -1352,6 +1361,7 @@ tcpcli_t *opentcpcli(const char *path, char *msg) return nullptr; } *tcpcli = tcpcli0; + tcpcli->svr.sock = -1; decodetcppath(path, tcpcli->svr.saddr, port, nullptr, nullptr, nullptr, nullptr); if (sscanf(port, "%d", &tcpcli->svr.port) < 1) { @@ -1371,7 +1381,10 @@ tcpcli_t *opentcpcli(const char *path, char *msg) void closetcpcli(tcpcli_t *tcpcli) { tracet(3, "closetcpcli: sock=%d\n", tcpcli->svr.sock); - closesocket(tcpcli->svr.sock); + if (tcpcli->svr.sock >= 0) + { + closesocket(tcpcli->svr.sock); + } free(tcpcli); } @@ -1524,6 +1537,19 @@ Rtklib_Tls_Client *ntrip_tls(ntrip_t *ntrip) return static_cast(ntrip->tls_ctx); } +void ntrip_clear_request(ntrip_t *ntrip) +{ + /* Requests can contain caster credentials. Use volatile stores so clearing + * the persistent retry buffer is not optimized away. */ + volatile unsigned char *request = ntrip->request_buff; + for (std::size_t i = 0; i < sizeof(ntrip->request_buff); ++i) + { + request[i] = 0; + } + ntrip->request_length = 0; + ntrip->request_offset = 0; +} + void ntrip_reset_tls(ntrip_t *ntrip) { if (ntrip->tls_ctx) @@ -1534,6 +1560,7 @@ void ntrip_reset_tls(ntrip_t *ntrip) void ntrip_disconnect(ntrip_t *ntrip, int tcon) { + ntrip_clear_request(ntrip); ntrip_reset_tls(ntrip); discontcp(&ntrip->tcp->svr, tcon); } @@ -1598,6 +1625,71 @@ int ntrip_write_transport(ntrip_t *ntrip, const unsigned char *buff, int n, char return ns; } +bool ntrip_prepare_request(ntrip_t *ntrip, const std::string &request, char *msg) +{ + if (request.empty() || request.size() > sizeof(ntrip->request_buff)) + { + std::snprintf(msg, MAXSTRMSG, "NTRIP request too large"); + ntrip->state = -1; + ntrip_disconnect(ntrip, -1); + return false; + } + + std::memcpy(ntrip->request_buff, request.data(), request.size()); + ntrip->request_length = static_cast(request.size()); + ntrip->request_offset = 0; + return true; +} + +int ntrip_flush_request(ntrip_t *ntrip, char *msg) +{ + if (ntrip->request_length <= 0 || + ntrip->request_offset < 0 || + ntrip->request_offset > ntrip->request_length) + { + std::snprintf(msg, MAXSTRMSG, "invalid NTRIP request state"); + ntrip->state = -1; + ntrip_disconnect(ntrip, -1); + return 0; + } + + const int remaining = ntrip->request_length - ntrip->request_offset; + const int ns = ntrip_write_transport(ntrip, + ntrip->request_buff + ntrip->request_offset, remaining, msg); + + /* A fatal transport error resets the request in ntrip_disconnect(). Plain + * TCP errors also leave the socket disconnected. In both cases, rebuild + * and resend the complete request on the next connection. */ + if (ntrip->tcp->svr.state != 2) + { + ntrip_clear_request(ntrip); + return 0; + } + if (ns <= 0) + { + /* The non-blocking transport would block. Keeping both the buffer + * address and the remaining length unchanged is required when an + * OpenSSL SSL_write() call returns WANT_READ or WANT_WRITE. */ + return 0; + } + if (ns > remaining) + { + std::snprintf(msg, MAXSTRMSG, "invalid NTRIP write result"); + ntrip->state = -1; + ntrip_disconnect(ntrip, -1); + return 0; + } + + ntrip->request_offset += ns; + if (ntrip->request_offset < ntrip->request_length) + { + return 0; + } + + ntrip_clear_request(ntrip); + return 1; +} + void ntrip_consume_buffer(ntrip_t *ntrip, int n) { if (n <= 0) @@ -1677,8 +1769,9 @@ int read_ntrip_chunked(ntrip_t *ntrip, unsigned char *buff, int n, char *msg) std::memcpy(size_text, ntrip->buff, eol); size_text[eol] = '\0'; char *end = nullptr; - const unsigned long chunk_size = std::strtoul(size_text, &end, 16); - if (end == size_text || chunk_size > UINT_MAX) + errno = 0; + const uint64_t chunk_size = std::strtoul(size_text, &end, 16); + if (end == size_text || errno == ERANGE || chunk_size > UINT_MAX) { std::snprintf(msg, MAXSTRMSG, "invalid HTTP chunk"); ntrip_disconnect(ntrip, ntrip->tcp->tirecon); @@ -1706,15 +1799,16 @@ int read_ntrip_chunked(ntrip_t *ntrip, unsigned char *buff, int n, char *msg) return copied; } - const int available = std::min(ntrip->nb, - static_cast(ntrip->chunk_remaining)); - const int take = std::min(available, n - copied); + const unsigned int available = std::min( + static_cast(ntrip->nb), ntrip->chunk_remaining); + const unsigned int take = std::min( + available, static_cast(n - copied)); if (take > 0) { std::memcpy(buff + copied, ntrip->buff, take); - ntrip_consume_buffer(ntrip, take); - ntrip->chunk_remaining -= static_cast(take); - copied += take; + ntrip_consume_buffer(ntrip, static_cast(take)); + ntrip->chunk_remaining -= take; + copied += static_cast(take); } if (ntrip->chunk_remaining == 0) { @@ -1749,6 +1843,223 @@ int read_ntrip_chunked(ntrip_t *ntrip, unsigned char *buff, int n, char *msg) return copied; } +void trim_http_ows(std::string *value) +{ + std::string::size_type first = 0; + while (first < value->size() && + ((*value)[first] == ' ' || (*value)[first] == '\t')) + { + ++first; + } + std::string::size_type last = value->size(); + while (last > first && + ((*value)[last - 1] == ' ' || (*value)[last - 1] == '\t')) + { + --last; + } + *value = value->substr(first, last - first); +} + +void lowercase_ascii(std::string *value) +{ + std::transform(value->begin(), value->end(), value->begin(), [](unsigned char c) { + return static_cast(c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c); + }); +} + +bool is_http_token_character(unsigned char c) +{ + const bool ascii_alphanumeric = + (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z'); + return ascii_alphanumeric || c == '!' || c == '#' || c == '$' || + c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || + c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || + c == '|' || c == '~'; +} + +bool is_ascii_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +bool parse_http_status_line(const char *begin, const char *end, int *status) +{ + const char prefix[] = "HTTP/"; + if (end - begin < static_cast(sizeof(prefix) - 1) || + std::memcmp(begin, prefix, sizeof(prefix) - 1) != 0) + { + return false; + } + + const char *p = begin + sizeof(prefix) - 1; + const char *number_begin = p; + while (p < end && is_ascii_digit(*p)) + { + ++p; + } + if (p == number_begin || p == end || *p++ != '.') + { + return false; + } + number_begin = p; + while (p < end && is_ascii_digit(*p)) + { + ++p; + } + if (p == number_begin || p == end || *p++ != ' ' || end - p < 3) + { + return false; + } + if (!is_ascii_digit(p[0]) || !is_ascii_digit(p[1]) || + !is_ascii_digit(p[2])) + { + return false; + } + *status = (p[0] - '0') * 100 + (p[1] - '0') * 10 + (p[2] - '0'); + p += 3; + if (p < end && *p++ != ' ') + { + return false; + } + while (p < end) + { + const auto c = static_cast(*p++); + if ((c < 0x20U && c != '\t') || c == 0x7FU) + { + return false; + } + } + return true; +} + +bool parse_transfer_encoding(const std::string &value, bool *chunked) +{ + std::string::size_type begin = 0; + do + { + const std::string::size_type end = value.find(',', begin); + std::string coding = value.substr(begin, + end == std::string::npos ? std::string::npos : end - begin); + trim_http_ows(&coding); + lowercase_ascii(&coding); + + /* GNSS-SDR only decodes the chunked transfer coding. Requiring an + * exact token avoids treating values such as "notchunked" as + * chunked, and rejecting other codings avoids feeding encoded data + * to the RTCM decoder. RFC 7230 also forbids applying chunked more + * than once. */ + if (coding != "chunked" || *chunked) + { + return false; + } + *chunked = true; + + if (end == std::string::npos) + { + break; + } + begin = end + 1; + } + while (begin <= value.size()); + return true; +} + +bool parse_ntrip_v2_headers(const char *begin, const char *headers_end, + bool *content_type_present, std::string *content_type, bool *chunked, + std::string *error) +{ + const char *cursor = begin; + while (cursor < headers_end) + { + const char *field_end = cursor; + while (field_end + 1 < headers_end && + !(field_end[0] == '\r' && field_end[1] == '\n')) + { + ++field_end; + } + if (field_end + 1 >= headers_end) + { + *error = "malformed NTRIP v2 response headers"; + return false; + } + + const char *colon = std::find(cursor, field_end, ':'); + if (colon == field_end || colon == cursor) + { + *error = "malformed NTRIP v2 response header"; + return false; + } + for (const char *p = cursor; p < colon; ++p) + { + if (!is_http_token_character(static_cast(*p))) + { + *error = "malformed NTRIP v2 response header name"; + return false; + } + } + for (const char *p = colon + 1; p < field_end; ++p) + { + const auto c = static_cast(*p); + if ((c < 0x20U && c != '\t') || c == 0x7FU) + { + *error = "malformed NTRIP v2 response header value"; + return false; + } + } + + std::string name(cursor, colon); + std::string value(colon + 1, field_end); + lowercase_ascii(&name); + trim_http_ows(&value); + + if (name == "content-type") + { + if (*content_type_present) + { + *error = "duplicate NTRIP Content-Type header"; + return false; + } + *content_type_present = true; + const std::string::size_type parameter = value.find(';'); + *content_type = value.substr(0, parameter); + trim_http_ows(content_type); + lowercase_ascii(content_type); + if (content_type->empty()) + { + *error = "empty NTRIP Content-Type header"; + return false; + } + } + else if (name == "transfer-encoding") + { + if (!parse_transfer_encoding(value, chunked)) + { + *error = "unsupported NTRIP Transfer-Encoding"; + return false; + } + } + + cursor = field_end + 2; + } + return cursor == headers_end; +} + +int reject_ntrip_v2_response(ntrip_t *ntrip, char *msg, + const std::string &reason) +{ + std::snprintf(msg, MAXSTRMSG, "%s", reason.c_str()); + tracet(1, "rspntrip_c: %s nb=%d\n", msg, ntrip->nb); + ntrip->nb = 0; + ntrip->buff[0] = '\0'; + ntrip->chunked = 0; + ntrip->chunk_state = 0; + ntrip->chunk_remaining = 0; + ntrip->state = 0; + ntrip_disconnect(ntrip, ntrip->tcp->tirecon); + return 0; +} + bool parse_ntrip_client_options(const char *path, std::string &clean_path, int &version, int &tls_enabled, char *msg) { @@ -1809,26 +2120,29 @@ bool parse_ntrip_client_options(const char *path, std::string &clean_path, /* send ntrip server request -------------------------------------------------*/ int reqntrip_s(ntrip_t *ntrip, char *msg) { - char buff[256 + NTRIP_MAXSTR]; - char *p = buff; - char *s; - s = p; - tracet(3, "reqntrip_s: state=%d\n", ntrip->state); - p += std::snprintf(p, 256 + NTRIP_MAXSTR, "SOURCE %s %s\r\n", ntrip->passwd, ntrip->mntpnt); - p += std::snprintf(p, NTRIP_MAXSTR - (p - s), "Source-Agent: NTRIP %s\r\n", NTRIP_AGENT); - p += std::snprintf(p, NTRIP_MAXSTR - (p - s), "STR: %s\r\n", ntrip->str); - p += std::snprintf(p, sizeof("\r\n") + 1, "\r\n"); + if (ntrip->request_length == 0) + { + std::ostringstream request; + request << "SOURCE " << ntrip->passwd << " " << ntrip->mntpnt << "\r\n"; + request << "Source-Agent: NTRIP " << NTRIP_AGENT << "\r\n"; + request << "STR: " << ntrip->str << "\r\n\r\n"; + if (!ntrip_prepare_request(ntrip, request.str(), msg)) + { + return 0; + } + } - if (writetcpcli(ntrip->tcp, reinterpret_cast(buff), p - buff, msg) != p - buff) + const int request_length = ntrip->request_length; + if (!ntrip_flush_request(ntrip, msg)) { return 0; } - tracet(2, "reqntrip_s: send request state=%d ns=%" PRIdPTR "\n", ntrip->state, p - buff); + tracet(2, "reqntrip_s: send request state=%d ns=%d\n", ntrip->state, request_length); /* The SOURCE request contains the caster password. */ - tracet(5, "reqntrip_s: request headers redacted n=%" PRIdPTR "\n", p - buff); + tracet(5, "reqntrip_s: request headers redacted n=%d\n", request_length); ntrip->state = 1; return 1; } @@ -1843,42 +2157,48 @@ int reqntrip_c(ntrip_t *ntrip, char *msg) tracet(3, "reqntrip_c: state=%d version=%d tls=%d\n", ntrip->state, ntrip->version, ntrip->tls_enabled); - std::string target = *ntrip->url ? std::string(ntrip->url) : std::string(); - target += "/"; - target += ntrip->mntpnt; - - std::ostringstream request; - request << "GET " << target << (ntrip->version == NTRIP_VERSION_2 ? " HTTP/1.1\r\n" : " HTTP/1.0\r\n"); - - if (ntrip->version == NTRIP_VERSION_2) + if (ntrip->request_length == 0) { - request << "Host: " << ntrip->host << ":" << ntrip->port << "\r\n"; - request << "Ntrip-Version: Ntrip/2.0\r\n"; - } - request << "User-Agent: NTRIP " << NTRIP_AGENT << "\r\n"; - request << "Accept: */*\r\n"; - request << "Connection: close\r\n"; + std::string target = *ntrip->url ? std::string(ntrip->url) : std::string(); + target += "/"; + target += ntrip->mntpnt; - if (*ntrip->user) - { - std::snprintf(user, sizeof(user), "%s:%s", ntrip->user, ntrip->passwd); - encbase64(authorization, reinterpret_cast(user), strlen(user)); - request << "Authorization: Basic " << authorization << "\r\n"; - } - request << "\r\n"; + std::ostringstream request; + request << "GET " << target << (ntrip->version == NTRIP_VERSION_2 ? " HTTP/1.1\r\n" : " HTTP/1.0\r\n"); - const std::string request_text = request.str(); - if (ntrip_write_transport(ntrip, - reinterpret_cast(request_text.data()), - static_cast(request_text.size()), msg) != static_cast(request_text.size())) + if (ntrip->version == NTRIP_VERSION_2) + { + request << "Host: " << ntrip->host << ":" << ntrip->port << "\r\n"; + request << "Ntrip-Version: Ntrip/2.0\r\n"; + } + request << "User-Agent: NTRIP " << NTRIP_AGENT << "\r\n"; + request << "Accept: */*\r\n"; + request << "Connection: close\r\n"; + + if (*ntrip->user) + { + std::snprintf(user, sizeof(user), "%s:%s", ntrip->user, ntrip->passwd); + encbase64(authorization, reinterpret_cast(user), strlen(user)); + request << "Authorization: Basic " << authorization << "\r\n"; + } + request << "\r\n"; + + if (!ntrip_prepare_request(ntrip, request.str(), msg)) + { + return 0; + } + } + + const int request_length = ntrip->request_length; + if (!ntrip_flush_request(ntrip, msg)) { return 0; } /* The request can contain a Basic Authorization header, so only sizes are * logged here. */ - tracet(2, "reqntrip_c: request sent state=%d version=%d ns=%zu\n", - ntrip->state, ntrip->version, request_text.size()); + tracet(2, "reqntrip_c: request sent state=%d version=%d ns=%d\n", + ntrip->state, ntrip->version, request_length); ntrip->state = 1; return 1; } @@ -1893,41 +2213,51 @@ int rspntrip_c_v2(ntrip_t *ntrip, char *msg) { if (ntrip->nb >= NTRIP_MAXRSP - 1) { - std::snprintf(msg, MAXSTRMSG, "response overflow"); - ntrip->state = 0; - ntrip_disconnect(ntrip, ntrip->tcp->tirecon); + return reject_ntrip_v2_response(ntrip, msg, + "NTRIP v2 response header overflow"); } return 0; } char *line_end = std::strstr(data, "\r\n"); - int http_major = 0; - int http_minor = 0; int status = 0; - if (!line_end || std::sscanf(data, "HTTP/%d.%d %d", &http_major, &http_minor, &status) != 3) + if (!line_end || line_end > header_end || + !parse_http_status_line(data, line_end, &status)) { - std::snprintf(msg, MAXSTRMSG, "invalid NTRIP v2 response"); - ntrip->state = 0; - ntrip_disconnect(ntrip, ntrip->tcp->tirecon); - return 0; + return reject_ntrip_v2_response(ntrip, msg, + "invalid NTRIP v2 HTTP status line"); } if (status != 200) { const int line_size = std::min(static_cast(line_end - data), MAXSTRMSG - 1); - std::memcpy(msg, data, line_size); - msg[line_size] = '\0'; - ntrip->state = 0; - ntrip_disconnect(ntrip, ntrip->tcp->tirecon); - return 0; + return reject_ntrip_v2_response(ntrip, msg, + std::string(data, static_cast(line_size))); + } + + bool content_type_present = false; + std::string content_type; + bool chunked = false; + std::string header_error; + if (!parse_ntrip_v2_headers(line_end + 2, header_end + 2, + &content_type_present, &content_type, &chunked, &header_error)) + { + return reject_ntrip_v2_response(ntrip, msg, header_error); + } + if (content_type_present) + { + const char *expected_type = *ntrip->mntpnt + ? "gnss/data" + : "gnss/sourcetable"; + if (content_type != expected_type) + { + return reject_ntrip_v2_response(ntrip, msg, + std::string("unexpected NTRIP Content-Type: ") + content_type); + } } const int header_size = static_cast(header_end + 4 - data); - std::string headers(data, header_size); - std::transform(headers.begin(), headers.end(), headers.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - ntrip->chunked = headers.find("transfer-encoding:") != std::string::npos && - headers.find("chunked") != std::string::npos; + ntrip->chunked = chunked ? 1 : 0; ntrip->chunk_state = 0; ntrip->chunk_remaining = 0; ntrip_consume_buffer(ntrip, header_size); @@ -2113,6 +2443,9 @@ int waitntrip(ntrip_t *ntrip, char *msg) if (ntrip->tcp->svr.state < 2) { ntrip->state = 0; /* tcp disconnected */ + /* A new connection must send the complete request, never just the + * suffix left from a short write on the previous socket. */ + ntrip_clear_request(ntrip); ntrip_reset_tls(ntrip); } @@ -2181,6 +2514,8 @@ ntrip_t *openntrip(const char *path, int type, char *msg) ntrip->chunk_state = 0; ntrip->chunk_remaining = 0; ntrip->nb = 0; + ntrip->request_length = 0; + ntrip->request_offset = 0; ntrip->tls_ctx = nullptr; ntrip->url[0] = '\0'; ntrip->host[0] = ntrip->port[0] = '\0'; @@ -2188,6 +2523,7 @@ ntrip_t *openntrip(const char *path, int type, char *msg) for (i = 0; i < NTRIP_MAXRSP; i++) { ntrip->buff[i] = 0; + ntrip->request_buff[i] = 0; } /* decode tcp/ntrip path */ @@ -2254,6 +2590,7 @@ ntrip_t *openntrip(const char *path, int type, char *msg) void closentrip(ntrip_t *ntrip) { tracet(3, "closentrip: state=%d\n", ntrip->state); + ntrip_clear_request(ntrip); delete ntrip_tls(ntrip); closetcpcli(ntrip->tcp); free(ntrip); diff --git a/src/algorithms/libs/rtklib/rtklib_tls.cc b/src/algorithms/libs/rtklib/rtklib_tls.cc index e41acd6e0..4072dacab 100644 --- a/src/algorithms/libs/rtklib/rtklib_tls.cc +++ b/src/algorithms/libs/rtklib/rtklib_tls.cc @@ -73,6 +73,11 @@ class Rtklib_Tls_Client::Impl public: explicit Impl(std::string hostname) : d_hostname(std::move(hostname)) {} + Impl(const Impl &) = delete; + Impl &operator=(const Impl &) = delete; + Impl(Impl &&) = delete; + Impl &operator=(Impl &&) = delete; + bool initialize(char *msg) { #ifdef USE_GNUTLS_FALLBACK diff --git a/src/algorithms/libs/rtklib/rtklib_tls.h b/src/algorithms/libs/rtklib/rtklib_tls.h index 4a3e64e48..e5b2a8e42 100644 --- a/src/algorithms/libs/rtklib/rtklib_tls.h +++ b/src/algorithms/libs/rtklib/rtklib_tls.h @@ -29,6 +29,8 @@ public: Rtklib_Tls_Client(const Rtklib_Tls_Client &) = delete; Rtklib_Tls_Client &operator=(const Rtklib_Tls_Client &) = delete; + Rtklib_Tls_Client(Rtklib_Tls_Client &&) = delete; + Rtklib_Tls_Client &operator=(Rtklib_Tls_Client &&) = delete; bool initialize(char *msg); void reset();