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