OpenCPN Partial API docs
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 
48 struct MemoryStruct {
49  char* memory;
50  size_t size;
51  MemoryStruct() {
52  memory = (char*)malloc(1);
53  size = 0;
54  }
55  ~MemoryStruct() { free(memory); }
56 };
57 
58 using PeerDlgPair = std::pair<PeerDlgResult, std::string>;
59 
60 PeerData::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 
67 static 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 
87 static 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 
109 static 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 
146 static 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 
166 static 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 
179 static 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 }
200 static 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 
226 bool 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 
240 void 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 
258 static 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 
322 static 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 
351 static 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 
387 static 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 
403 static 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 
443 bool 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 
460 bool 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
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
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
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
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.
Definition: semantic_vers.h:51
static SemanticVersion parse(std::string s)
Parse a version string, sets major == -1 on errors.