OpenCPN Partial API docs
Loading...
Searching...
No Matches
peer_client.cpp
Go to the documentation of this file.
1/***************************************************************************
2 * Copyright (C) 2022 by David Register *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, see <https://www.gnu.org/licenses/>. *
16 **************************************************************************/
17
24#include <iostream>
25#include <sstream>
26#include <string>
27#include <unordered_map>
28#include <utility>
29
30#include <curl/curl.h>
31
32#include "nlohmann/json.hpp"
33
34#include <wx/fileconf.h>
35#include <wx/log.h>
36#include <wx/string.h>
37
38#include "observable/configvar.h"
39
40#include "model/config_vars.h"
41#include "model/nav_object_database.h"
42#include "model/peer_client.h"
43#include "model/ocpn_utils.h"
44#include "model/rest_server.h"
45#include "model/semantic_vers.h"
46
48 char* memory;
49 size_t size;
50 MemoryStruct() {
51 memory = (char*)malloc(1);
52 size = 0;
53 }
54 ~MemoryStruct() { free(memory); }
55};
56
57using PeerDlgPair = std::pair<PeerDlgResult, std::string>;
58
59PeerData::PeerData(obs::EventVar& p)
60 : overwrite(false),
61 activate(false),
62 progress(p),
63 run_status_dlg([](PeerDlg, int) { return PeerDlgResult::Cancel; }),
64 run_pincode_dlg([] { return PeerDlgPair(PeerDlgResult::Cancel, ""); }) {}
65
66static size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb,
67 void* userp) {
68 size_t realsize = size * nmemb;
69 struct MemoryStruct* mem = (struct MemoryStruct*)userp;
70
71 char* ptr = (char*)realloc(mem->memory, mem->size + realsize + 1);
72 if (!ptr) {
73 /* out of memory! */
74 std::cerr << "not enough memory (realloc returned NULL)\n";
75 return 0;
76 }
77
78 mem->memory = ptr;
79 memcpy(&(mem->memory[mem->size]), contents, realsize);
80 mem->size += realsize;
81 mem->memory[mem->size] = 0;
82
83 return realsize;
84}
85
86static int xfer_callback(void* clientp, [[maybe_unused]] curl_off_t dltotal,
87 [[maybe_unused]] curl_off_t dlnow, curl_off_t ultotal,
88 curl_off_t ulnow) {
89 auto peer_data = static_cast<PeerData*>(clientp);
90 if (ultotal == 0) {
91 peer_data->progress.Notify(0, "");
92 } else {
93 peer_data->progress.Notify(100 * ulnow / ultotal, "");
94 }
95// FIXME (leamas) dirty fix for outdated, bundled curl
96// returning 0 is undocumented, but worked for 5.8
97#ifdef CURL_PROGRESSFUNC_CONTINUE
98 return CURL_PROGRESSFUNC_CONTINUE;
99#else
100 return 0;
101#endif
102}
103
108static long ApiPost(const std::string& url, const std::string& body,
109 PeerData& peer_data, MemoryStruct* response) {
110 long response_code = -1;
111 peer_data.progress.Notify(0, "");
112
113 CURL* c = curl_easy_init();
114 // No encoding, plain ASCII
115 curl_easy_setopt(c, CURLOPT_ENCODING, "identity"); // Plain ASCII
116 curl_easy_setopt(c, CURLOPT_URL, url.c_str());
117 curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, 0L);
118 curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, 0L);
119
120 curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, body.size());
121 curl_easy_setopt(c, CURLOPT_COPYPOSTFIELDS, body.c_str());
122 curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
123 curl_easy_setopt(c, CURLOPT_WRITEDATA, (void*)response);
124 curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0);
125 curl_easy_setopt(c, CURLOPT_XFERINFODATA, &peer_data);
126 curl_easy_setopt(c, CURLOPT_XFERINFOFUNCTION, xfer_callback);
127 curl_easy_setopt(c, CURLOPT_TIMEOUT, 20);
128 // FIXME (leamas) always logs
129 curl_easy_setopt(c, CURLOPT_VERBOSE,
130 wxLog::GetLogLevel() >= wxLOG_Debug ? 1 : 0);
131
132 CURLcode result = curl_easy_perform(c);
133 peer_data.progress.Notify(0, "");
134 if (result == CURLE_OK)
135 curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code);
136
137 curl_easy_cleanup(c);
138 return response_code == -1 ? -static_cast<long>(result) : response_code;
139}
140
145static int ApiGet(const std::string& url, const MemoryStruct* chunk,
146 int timeout = 0) {
147 long response_code = -1;
148
149 CURL* c = curl_easy_init();
150 curl_easy_setopt(c, CURLOPT_ENCODING, "identity"); // Encoding: plain ASCII
151 curl_easy_setopt(c, CURLOPT_URL, url.c_str());
152 curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, 0L);
153 curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, 0L);
154 curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
155 curl_easy_setopt(c, CURLOPT_WRITEDATA, (void*)chunk);
156 curl_easy_setopt(c, CURLOPT_NOPROGRESS, 1);
157 if (timeout != 0) curl_easy_setopt(c, CURLOPT_TIMEOUT, timeout);
158 CURLcode result = curl_easy_perform(c);
159 if (result == CURLE_OK)
160 curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &response_code);
161 curl_easy_cleanup(c);
162 return response_code == -1 ? -static_cast<long>(result) : response_code;
163}
164
165static std::string GetClientKey(std::string& server_name) {
166 obs::ConfigVar<std::string> server_keys("/Settings/RESTClient", "ServerKeys",
167 TheBaseConfig());
168 auto key_string = server_keys.Get("");
169 auto entries = ocpn::split(key_string.c_str(), ";");
170 for (const auto& entry : entries) {
171 auto server_key = ocpn::split(entry.c_str(), ":");
172 if (server_key.size() != 2) continue;
173 if (server_key[0] == server_name) return server_key[1];
174 }
175 return "1";
176}
177
178static void SaveClientKey(std::string& server_name, std::string key) {
179 obs::ConfigVar<std::string> server_keys("/Settings/RESTClient", "ServerKeys",
180 TheBaseConfig());
181 auto config_server_keys = server_keys.Get("");
182
183 auto server_keys_list = ocpn::split(config_server_keys.c_str(), ";");
184 std::unordered_map<std::string, std::string> key_by_server;
185 for (const auto& item : server_keys_list) {
186 auto server_and_key = ocpn::split(item.c_str(), ":");
187 if (server_and_key.size() != 2) continue;
188 key_by_server[server_and_key[0]] = server_and_key[1];
189 }
190 key_by_server[server_name] = key;
191
192 config_server_keys = "";
193 for (const auto& it : key_by_server) {
194 config_server_keys += it.first + ":" + it.second + ";";
195 }
196 server_keys.Set(config_server_keys);
197 wxLog::FlushActive();
198}
199static RestServerResult ParseServerJson(const MemoryStruct& reply,
200 PeerData& peer_data) {
201 std::string body(reply.memory);
202 nlohmann::json root;
203 try {
204 root = nlohmann::json::parse(body);
205 } catch (nlohmann::json::exception& e) {
206 wxLogMessage("Json server reply parse error: %s", e.what());
207 peer_data.run_status_dlg(PeerDlg::JsonParseError, 1);
208 peer_data.api_version = SemanticVersion(-1, -1);
209 return RestServerResult::Void;
210 }
211 if (root.contains("version")) {
212 auto s = root["version"].get<std::string>();
213 peer_data.api_version = SemanticVersion::parse(s);
214 }
215 if (root.contains("result")) {
216 return static_cast<RestServerResult>(root["result"].get<int>());
217 } else {
218 return RestServerResult::Void;
219 }
220}
221
222bool CheckKey(const std::string& key, PeerData peer_data) {
223 std::stringstream url;
224 url << "https://" << peer_data.dest_ip_address << "/api/ping"
225 << "?source=" << g_hostname << "&apikey=" << key;
226 MemoryStruct reply;
227 long status = ApiGet(url.str(), &reply, 5);
228 if (status != 200) {
229 peer_data.run_status_dlg(PeerDlg::InvalidHttpResponse, status);
230 return false;
231 }
232 auto result = ParseServerJson(reply, peer_data);
233 return result != RestServerResult::NewPinRequested;
234}
235
236void GetApiVersion(PeerData& peer_data) {
237 if (peer_data.api_version > SemanticVersion(5, 0)) return;
238 std::stringstream url;
239 url << "https://" << peer_data.dest_ip_address << "/api/get-version";
240
241 struct MemoryStruct chunk;
242 std::string buf;
243 long response_code = ApiGet(url.str(), &chunk, 2);
244
245 if (response_code == 200) {
246 ParseServerJson(chunk, peer_data);
247 } else {
248 // Return "old" version without /api/writable support
249 peer_data.api_version = SemanticVersion(5, 8);
250 }
251}
252
254static bool GetApiKey(PeerData& peer_data, std::string& key) {
255 std::string api_key;
256 if (peer_data.api_version == SemanticVersion(0, 0)) GetApiVersion(peer_data);
257
258 while (true) {
259 api_key = GetClientKey(peer_data.server_name);
260 if (api_key.size() < 9 && peer_data.api_version >= SemanticVersion(5, 9))
261 api_key = "0123456789abc"; // Long enough for being seen as 5.9+
262 std::stringstream url;
263 url << "https://" << peer_data.dest_ip_address << "/api/ping"
264 << "?source=" << g_hostname << "&apikey=" << api_key;
265 MemoryStruct chunk;
266 int status = ApiGet(url.str(), &chunk, 3);
267 if (status != 200) {
268 auto r = peer_data.run_status_dlg(PeerDlg::InvalidHttpResponse, status);
269 if (r == PeerDlgResult::Ok) continue;
270 return false;
271 }
272 auto result = ParseServerJson(chunk, peer_data);
273 switch (result) {
274 case RestServerResult::NewPinRequested: {
275 auto pin_result = peer_data.run_pincode_dlg();
276 if (pin_result.first == PeerDlgResult::HasPincode) {
277 std::string tentative_pin = ocpn::trim(pin_result.second);
278 unsigned int_pin = atoi(tentative_pin.c_str());
279 Pincode pincode(int_pin);
280 api_key = pincode.Hash();
281 GetApiVersion(peer_data);
282 if (peer_data.api_version < SemanticVersion(5, 9)) {
283 api_key = pincode.CompatHash();
284 }
285 if (!CheckKey(api_key, peer_data)) {
286 auto r = peer_data.run_status_dlg(PeerDlg::BadPincode, 0);
287 if (r == PeerDlgResult::Ok) continue;
288 return false;
289 }
290 SaveClientKey(peer_data.server_name, api_key);
291 } else if (pin_result.first == PeerDlgResult::Cancel) {
292 return false;
293 } else {
294 auto r = peer_data.run_status_dlg(PeerDlg::ErrorReturn,
295 static_cast<int>(result));
296 if (r == PeerDlgResult::Ok) continue;
297 return false;
298 }
299 } break;
300 case RestServerResult::GenericError:
301 // 5.8 returns GenericError for a valid key (!)
302 [[fallthrough]];
303 case RestServerResult::NoError:
304 break;
305 default:
306 auto r = peer_data.run_status_dlg(PeerDlg::ErrorReturn,
307 static_cast<int>(result));
308 if (r == PeerDlgResult::Ok) continue;
309 return false;
310 }
311 break;
312 }
313 key = api_key;
314 return true;
315}
316
318static std::string PeerDataToXml(PeerData& peer_data) {
320 std::ostringstream stream;
321 int total = peer_data.routes.size() + peer_data.tracks.size() +
322 peer_data.routepoints.size();
323 int gpxgen = 0;
324 for (auto r : peer_data.routes) {
325 gpxgen++;
326 gpx.AddGPXRoute(r);
327 peer_data.progress.Notify(100 * gpxgen / total, "");
328 wxYield();
329 }
330 for (auto r : peer_data.routepoints) {
331 gpxgen++;
332 gpx.AddGPXWaypoint(r);
333 peer_data.progress.Notify(100 * gpxgen / total, "");
334 wxYield();
335 }
336 for (auto r : peer_data.tracks) {
337 gpxgen++;
338 gpx.AddGPXTrack(r);
339 peer_data.progress.Notify(100 * gpxgen / total, "");
340 wxYield();
341 }
342 gpx.save(stream, PUGIXML_TEXT(" "));
343 return stream.str();
344}
345
347static void SendObjects(std::string& body, const std::string& api_key,
348 PeerData& peer_data) {
349 bool cancel = false;
350 while (!cancel) {
351 std::stringstream url;
352 url << "https://" << peer_data.dest_ip_address << "/api/rx_object"
353 << "?source=" << g_hostname << "&apikey=" << api_key;
354 if (peer_data.overwrite) url << "&force=1";
355 if (peer_data.activate) url << "&activate=1";
356
357 struct MemoryStruct chunk;
358 long response_code = ApiPost(url.str(), body, peer_data, &chunk);
359 if (response_code == 200) {
360 std::string json(chunk.memory);
361 nlohmann::json root;
362 try {
363 root = nlohmann::json::parse(json);
364 } catch (nlohmann::json::exception& e) {
365 wxLogDebug("SendObjects, parse errors: %s", e.what());
366 }
367 // Capture the result
368 int result = root["result"].get<int>();
369 if (result > 0) {
370 peer_data.run_status_dlg(PeerDlg::ErrorReturn, result);
371 } else {
372 peer_data.run_status_dlg(PeerDlg::TransferOk, 0);
373 }
374 cancel = true;
375 } else {
376 peer_data.run_status_dlg(PeerDlg::InvalidHttpResponse, response_code);
377 cancel = true;
378 }
379 }
380}
381
383static int CheckChunk(struct MemoryStruct& chunk, const std::string& guid) {
384 std::string body(chunk.memory);
385 nlohmann::json root;
386 try {
387 root = nlohmann::json::parse(body);
388 } catch (nlohmann::json::exception& e) {
389 wxLogDebug("CheckChunk: parsing errors found: %s", e.what());
390 }
391 if (!root.contains("result")) return 0;
392 int result = root["result"].get<int>();
393 if (result != 0)
394 wxLogDebug("Server rejected guid %s, status: %d", guid.c_str(), result);
395 return result;
396}
397
399static bool CheckObjects(const std::string& api_key, PeerData& peer_data) {
400 std::stringstream url;
401 url << "https://" << peer_data.dest_ip_address << "/api/writable"
402 << "?source=" << g_hostname << "&apikey=" << api_key << "&guid=";
403 for (const auto& r : peer_data.routes) {
404 std::string guid = r->GetGUID().ToStdString();
405 std::string full_url = url.str() + guid;
406 struct MemoryStruct chunk;
407 if (ApiGet(full_url, &chunk) != 200) {
408 wxLogMessage("Cannot check /api/writable for route %s", guid.c_str());
409 return false;
410 }
411 int result = CheckChunk(chunk, guid);
412 if (result != 0) return false;
413 }
414 for (const auto& t : peer_data.tracks) {
415 std::string guid = t->m_GUID.ToStdString();
416 std::string full_url = url.str() + guid;
417 struct MemoryStruct chunk;
418 if (ApiGet(full_url, &chunk) != 200) {
419 wxLogMessage("Cannot check /api/writable for track %s", guid.c_str());
420 return false;
421 }
422 int result = CheckChunk(chunk, guid);
423 if (result != 0) return false;
424 }
425 for (const auto& rp : peer_data.routepoints) {
426 std::string guid = rp->m_GUID.ToStdString();
427 std::string full_url = url.str() + guid;
428 struct MemoryStruct chunk;
429 if (ApiGet(full_url, &chunk) != 200) {
430 wxLogMessage("Cannot check /api/writable for waypoint %s", guid.c_str());
431 return false;
432 }
433 int result = CheckChunk(chunk, guid);
434 if (result != 0) return false;
435 }
436 return true;
437}
438
439bool SendNavobjects(PeerData& peer_data) {
440 if (peer_data.routes.empty() && peer_data.routepoints.empty() &&
441 peer_data.tracks.empty()) {
442 return true;
443 }
444 std::string api_key;
445 bool apikey_ok = GetApiKey(peer_data, api_key);
446 if (!apikey_ok) return false;
447 if (peer_data.api_version < SemanticVersion(5, 9) && peer_data.activate) {
448 peer_data.run_status_dlg(PeerDlg::ActivateUnsupported, 0);
449 return false;
450 }
451 std::string body = PeerDataToXml(peer_data);
452 SendObjects(body, api_key, peer_data);
453 return true;
454}
455
456bool CheckNavObjects(PeerData& peer_data) {
457 if (peer_data.routes.empty() && peer_data.routepoints.empty() &&
458 peer_data.tracks.empty()) {
459 return true; // the server will not object to null transfers.
460 }
461 std::string apikey;
462 bool apikey_ok = GetApiKey(peer_data, apikey);
463 if (!apikey_ok) return false;
464 return CheckObjects(apikey, peer_data);
465}
A random generated int value with accessors for string and hashcode.
Definition pincode.h:31
Global variables stored in configuration file.
std::vector< std::string > split(const char *token_string, const std::string &delimiter)
Return vector of items in s separated by delimiter.
std::string trim(std::string s)
Strip possibly trailing and/or leading space characters in s.
Miscellaneous utilities, many of which string related.
bool SendNavobjects(PeerData &peer_data)
Send data to server peer.
bool CheckNavObjects(PeerData &peer_data)
Check if server peer deems that writing these objects can be accepted i.
Peer client non-gui abstraction.
REST API server.
RestServerResult
Return codes from HandleServerMessage and eventually in the http response.
Definition rest_server.h:55
Semantic version encode/decode object.
bool activate
API parameter, activate route after transfer.
Definition peer_client.h:58
std::function< std::pair< PeerDlgResult, std::string >()> run_pincode_dlg
Pin confirm dialog, returns new {0, user_pin} or {error_code, error msg)
Definition peer_client.h:70
SemanticVersion api_version
server API version
Definition peer_client.h:53
std::function< PeerDlgResult(PeerDlg, int)> run_status_dlg
Dialog displaying status (good, bad, ...)
Definition peer_client.h:64
obs::EventVar & progress
Notified with transfer percent progress (0-100).
Definition peer_client.h:61
bool overwrite
API parameter, force overwrite w/o server dialogs.
Definition peer_client.h:57
Versions uses a modified semantic versioning scheme: major.minor.revision.post-tag+build.
static SemanticVersion parse(std::string s)
Parse a version string, sets major == -1 on errors.