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