1 #include "xml_file.h"
2 #include "../include/version.h"
3
4 #include <libfilezilla/file.hpp>
5 #include <libfilezilla/local_filesys.hpp>
6 #include <libfilezilla/translate.hpp>
7
8 #include <cstring>
9
10 //TODO: move these file functions to libfilezilla
rename_file(std::wstring const & source,std::wstring const & dest)11 static bool rename_file(std::wstring const& source, std::wstring const& dest) {
12 #ifdef FZ_WINDOWS
13 return MoveFileW(source.c_str(), dest.c_str()) != 0;
14 #else
15 return std::rename(fz::to_native(source).c_str(), fz::to_native(dest).c_str()) == 0;
16 #endif
17 }
18
file_exists(std::wstring const & file)19 static bool file_exists(std::wstring const& file) {
20 return fz::local_filesys::get_file_type(fz::to_native(file), false) != fz::local_filesys::unknown;
21 }
22
copy_file(std::wstring const & src,std::wstring const & dest,bool overwrite=true,bool fsync=false)23 static bool copy_file(std::wstring const& src, std::wstring const& dest, bool overwrite = true, bool fsync = false)
24 {
25 if (!overwrite && file_exists(dest)) {
26 return false;
27 }
28
29 char buffer[8192];
30 fz::file in(fz::to_native(src), fz::file::reading, fz::file::existing);
31 fz::file out(fz::to_native(dest), fz::file::writing, fz::file::empty);
32
33 if (!in.opened() || !out.opened()) {
34 return false;
35 }
36
37 int64_t res = 0;
38 do {
39 res = in.read(buffer, sizeof(buffer));
40 if (res > 0) {
41 res = out.write(buffer, res);
42 }
43 } while(res > 0);
44
45 if (!res && fsync) {
46 out.fsync();
47 }
48
49 return res == 0;
50 }
51
CXmlFile(std::wstring const & fileName,std::string const & root)52 CXmlFile::CXmlFile(std::wstring const& fileName, std::string const& root)
53 {
54 if (!root.empty()) {
55 m_rootName = root;
56 }
57 SetFileName(fileName);
58 }
59
SetFileName(std::wstring const & name)60 void CXmlFile::SetFileName(std::wstring const& name)
61 {
62 m_fileName = name;
63 m_modificationTime = fz::datetime();
64 }
65
Load(bool overwriteInvalid)66 pugi::xml_node CXmlFile::Load(bool overwriteInvalid)
67 {
68 Close();
69 m_error.clear();
70
71 if (m_fileName.empty()) {
72 return m_element;
73 }
74
75 std::wstring redirectedName = GetRedirectedName();
76
77 GetXmlFile(redirectedName);
78 if (!m_element) {
79 std::wstring err = fz::sprintf(fztranslate("The file '%s' could not be loaded."), m_fileName);
80 if (m_error.empty()) {
81 err += L"\n" + fztranslate("Make sure the file can be accessed and is a well-formed XML document.");
82 }
83 else {
84 err += L"\n" + m_error;
85 }
86
87 // Try the backup file
88 GetXmlFile(redirectedName + L"~");
89 if (!m_element) {
90 // Loading backup failed.
91
92 // Create new one if we are allowed to create empty file
93 bool createEmpty = overwriteInvalid;
94
95 // Also, if both original and backup file are empty, create new file.
96 if (fz::local_filesys::get_size(fz::to_native(redirectedName)) <= 0 && fz::local_filesys::get_size(fz::to_native(redirectedName + L"~")) <= 0) {
97 createEmpty = true;
98 }
99
100 if (createEmpty) {
101 m_error.clear();
102 CreateEmpty();
103 m_modificationTime = fz::local_filesys::get_modification_time(fz::to_native(redirectedName));
104 return m_element;
105 }
106
107 // File corrupt and no functional backup, give up.
108 m_error = err;
109 m_modificationTime.clear();
110 return m_element;
111 }
112
113 // Loading the backup file succeeded, restore file
114 if (!copy_file(redirectedName + L"~", redirectedName, true, true)) {
115 // Could not restore backup, give up.
116 Close();
117 m_error = err;
118 m_error += L"\n" + fz::sprintf(fztranslate("The valid backup file %s could not be restored"), redirectedName + L"~");
119 m_modificationTime.clear();
120 return m_element;
121 }
122
123 // We no longer need the backup
124 fz::remove_file(fz::to_native(redirectedName + L"~"));
125 m_error.clear();
126 }
127
128 m_modificationTime = fz::local_filesys::get_modification_time(fz::to_native(redirectedName));
129 return m_element;
130 }
131
Modified() const132 bool CXmlFile::Modified() const
133 {
134 if (m_fileName.empty()) {
135 return false;
136 }
137
138 if (m_modificationTime.empty()) {
139 return true;
140 }
141
142 fz::datetime const modificationTime = fz::local_filesys::get_modification_time(fz::to_native(m_fileName));
143 if (!modificationTime.empty() && modificationTime == m_modificationTime) {
144 return false;
145 }
146
147 return true;
148 }
149
Close()150 void CXmlFile::Close()
151 {
152 m_element = pugi::xml_node();
153 m_document.reset();
154 }
155
UpdateMetadata()156 void CXmlFile::UpdateMetadata()
157 {
158 if (!m_element || std::string(m_element.name()) != "FileZilla3") {
159 return;
160 }
161
162 SetTextAttribute(m_element, "version", GetFileZillaVersion());
163
164 std::string const platform =
165 #ifdef FZ_WINDOWS
166 "windows";
167 #elif defined(FZ_MAC)
168 "mac";
169 #else
170 "*nix";
171 #endif
172 SetTextAttributeUtf8(m_element, "platform", platform);
173 }
174
Save(bool updateMetadata)175 bool CXmlFile::Save(bool updateMetadata)
176 {
177 m_error.clear();
178
179 if (m_fileName.empty() || !m_document) {
180 return false;
181 }
182
183 if (updateMetadata) {
184 UpdateMetadata();
185 }
186
187 bool res = SaveXmlFile();
188 m_modificationTime = fz::local_filesys::get_modification_time(fz::to_native(m_fileName));
189
190 return res;
191 }
192
CreateEmpty()193 pugi::xml_node CXmlFile::CreateEmpty()
194 {
195 Close();
196
197 pugi::xml_node decl = m_document.append_child(pugi::node_declaration);
198 decl.append_attribute("version") = "1.0";
199 decl.append_attribute("encoding") = "UTF-8";
200
201 m_element = m_document.append_child(m_rootName.c_str());
202 return m_element;
203 }
204
205 // Opens the specified XML file if it exists or creates a new one otherwise.
206 // Returns false on error.
GetXmlFile(std::wstring const & file)207 bool CXmlFile::GetXmlFile(std::wstring const& file)
208 {
209 Close();
210
211 if (fz::local_filesys::get_size(fz::to_native(file)) <= 0) {
212 return false;
213 }
214
215 // File exists, open it
216 auto result = m_document.load_file(static_cast<wchar_t const*>(file.c_str()));
217 if (!result) {
218 m_error += fz::sprintf(L"%s at offset %d.", result.description(), result.offset);
219 return false;
220 }
221
222 m_element = m_document.child(m_rootName.c_str());
223 if (!m_element) {
224 if (m_document.first_child()) { // Beware: parse_declaration and parse_doctype can break this
225 // Not created by FileZilla3
226 Close();
227 m_error = fztranslate("Unknown root element, the file does not appear to be generated by FileZilla.");
228 return false;
229 }
230 m_element = m_document.append_child(m_rootName.c_str());
231 }
232
233 return true;
234 }
235
GetRedirectedName() const236 std::wstring CXmlFile::GetRedirectedName() const
237 {
238 std::wstring redirectedName = m_fileName;
239 bool isLink = false;
240 if (fz::local_filesys::get_file_info(fz::to_native(redirectedName), isLink, 0, 0, 0) == fz::local_filesys::file) {
241 if (isLink) {
242 CLocalPath target(fz::to_wstring(fz::local_filesys::get_link_target(fz::to_native(redirectedName))));
243 if (!target.empty()) {
244 redirectedName = target.GetPath();
245 redirectedName.pop_back();
246 }
247 }
248 }
249 return redirectedName;
250 }
251
SaveXmlFile()252 bool CXmlFile::SaveXmlFile()
253 {
254 bool exists = false;
255 bool isLink = false;
256 int flags = 0;
257
258 std::wstring redirectedName = GetRedirectedName();
259 if (fz::local_filesys::get_file_info(fz::to_native(redirectedName), isLink, 0, 0, &flags) == fz::local_filesys::file) {
260 #ifdef FZ_WINDOWS
261 if (flags & FILE_ATTRIBUTE_HIDDEN) {
262 SetFileAttributes(redirectedName.c_str(), flags & ~FILE_ATTRIBUTE_HIDDEN);
263 }
264 #endif
265
266 exists = true;
267 if (!copy_file(redirectedName, redirectedName + L"~", true, true)) {
268 m_error = fztranslate("Failed to create backup copy of xml file");
269 return false;
270 }
271 }
272
273 struct flushing_xml_writer final : public pugi::xml_writer
274 {
275 public:
276 static bool save(pugi::xml_document const& document, std::wstring const& filename)
277 {
278 flushing_xml_writer writer(filename);
279 if (!writer.file_.opened()) {
280 return false;
281 }
282 document.save(writer);
283
284 return writer.file_.opened() && writer.file_.fsync();
285 }
286
287 private:
288 flushing_xml_writer(std::wstring const& filename)
289 : file_(fz::to_native(filename), fz::file::writing, fz::file::empty)
290 {
291 }
292
293 virtual void write(void const* data, size_t size) override {
294 if (file_.opened()) {
295 if (file_.write(data, static_cast<int64_t>(size)) != static_cast<int64_t>(size)) {
296 file_.close();
297 }
298 }
299 }
300
301 fz::file file_;
302 };
303
304 bool success = flushing_xml_writer::save(m_document, redirectedName);
305 if (!success) {
306 fz::remove_file(fz::to_native(redirectedName));
307 if (exists) {
308 rename_file(redirectedName + L"~", redirectedName);
309 }
310 m_error = fztranslate("Failed to write xml file");
311 return false;
312 }
313
314 if (exists) {
315 fz::remove_file(fz::to_native(redirectedName + L"~"));
316 }
317
318 return true;
319 }
320
GetServer(pugi::xml_node node,Site & site)321 bool GetServer(pugi::xml_node node, Site & site)
322 {
323 std::wstring host = GetTextElement(node, "Host");
324 if (host.empty()) {
325 return false;
326 }
327
328 unsigned int const port = node.child("Port").text().as_uint();
329 if (port < 1 || port > 65535) {
330 return false;
331 }
332
333 if (!site.server.SetHost(host, port)) {
334 return false;
335 }
336
337 int const protocol = node.child("Protocol").text().as_int();
338 if (protocol < 0 || protocol > ServerProtocol::MAX_VALUE) {
339 return false;
340 }
341 site.server.SetProtocol(static_cast<ServerProtocol>(protocol));
342
343 int type = GetTextElementInt(node, "Type");
344 if (type < 0 || type >= SERVERTYPE_MAX) {
345 return false;
346 }
347
348 site.server.SetType(static_cast<ServerType>(type));
349
350 int logonType = GetTextElementInt(node, "Logontype");
351 if (logonType < 0 || logonType >= static_cast<int>(LogonType::count)) {
352 return false;
353 }
354
355 site.SetLogonType(static_cast<LogonType>(logonType));
356
357 if (site.credentials.logonType_ != LogonType::anonymous) {
358 std::wstring user;
359
360 bool const has_user = ProtocolHasUser(site.server.GetProtocol());
361 if (has_user) {
362 user = GetTextElement(node, "User");
363 if (user.empty() && site.credentials.logonType_ != LogonType::interactive && site.credentials.logonType_ != LogonType::ask) {
364 return false;
365 }
366 }
367
368 std::wstring pass, key;
369 if (site.credentials.logonType_ == LogonType::normal || site.credentials.logonType_ == LogonType::account) {
370 auto passElement = node.child("Pass");
371 if (passElement) {
372 std::wstring encoding = GetTextAttribute(passElement, "encoding");
373
374 if (encoding == L"base64") {
375 std::string decoded = fz::base64_decode_s(passElement.child_value());
376 pass = fz::to_wstring_from_utf8(decoded);
377 }
378 else if (encoding == L"crypt") {
379 pass = fz::to_wstring_from_utf8(passElement.child_value());
380 site.credentials.encrypted_ = fz::public_key::from_base64(passElement.attribute("pubkey").value());
381 if (!site.credentials.encrypted_) {
382 pass.clear();
383 site.SetLogonType(LogonType::ask);
384 }
385 }
386 else if (!encoding.empty()) {
387 site.SetLogonType(LogonType::ask);
388 }
389 else {
390 pass = GetTextElement(passElement);
391 }
392 }
393
394 if (pass.empty() && !has_user) {
395 return false;
396 }
397 }
398 else if (site.credentials.logonType_ == LogonType::key) {
399 if (user.empty()) {
400 return false;
401 }
402
403 key = GetTextElement(node, "Keyfile");
404
405 // password should be empty if we're using a key file
406 pass.clear();
407
408 site.credentials.keyFile_ = key;
409 }
410
411 site.SetUser(user);
412 site.credentials.SetPass(pass);
413
414 site.credentials.account_ = GetTextElement(node, "Account");
415 }
416
417 int timezoneOffset = GetTextElementInt(node, "TimezoneOffset");
418 if (!site.server.SetTimezoneOffset(timezoneOffset)) {
419 return false;
420 }
421
422 std::string_view pasvMode = node.child_value("PasvMode");
423 if (pasvMode == "MODE_PASSIVE") {
424 site.server.SetPasvMode(MODE_PASSIVE);
425 }
426 else if (pasvMode == "MODE_ACTIVE") {
427 site.server.SetPasvMode(MODE_ACTIVE);
428 }
429 else {
430 site.server.SetPasvMode(MODE_DEFAULT);
431 }
432
433 int maximumMultipleConnections = GetTextElementInt(node, "MaximumMultipleConnections");
434 site.server.MaximumMultipleConnections(maximumMultipleConnections);
435
436 std::string_view encodingType = node.child_value("EncodingType");
437 if (encodingType == "UTF-8") {
438 site.server.SetEncodingType(ENCODING_UTF8);
439 }
440 else if (encodingType == "Custom") {
441 std::wstring customEncoding = GetTextElement(node, "CustomEncoding");
442 if (customEncoding.empty()) {
443 return false;
444 }
445 if (!site.server.SetEncodingType(ENCODING_CUSTOM, customEncoding)) {
446 return false;
447 }
448 }
449 else {
450 site.server.SetEncodingType(ENCODING_AUTO);
451 }
452
453 if (CServer::ProtocolHasFeature(site.server.GetProtocol(), ProtocolFeature::PostLoginCommands)) {
454 std::vector<std::wstring> postLoginCommands;
455 auto element = node.child("PostLoginCommands");
456 if (element) {
457 for (auto commandElement = element.child("Command"); commandElement; commandElement = commandElement.next_sibling("Command")) {
458 std::wstring command = fz::to_wstring_from_utf8(commandElement.child_value());
459 if (!command.empty()) {
460 postLoginCommands.emplace_back(std::move(command));
461 }
462 }
463 }
464 if (!site.server.SetPostLoginCommands(postLoginCommands)) {
465 return false;
466 }
467 }
468
469 site.server.SetBypassProxy(GetTextElementInt(node, "BypassProxy", false) == 1);
470 site.SetName(GetTextElement_Trimmed(node, "Name"));
471
472 if (site.GetName().empty()) {
473 site.SetName(GetTextElement_Trimmed(node));
474 }
475
476 for (auto parameter = node.child("Parameter"); parameter; parameter = parameter.next_sibling("Parameter")) {
477 site.server.SetExtraParameter(parameter.attribute("Name").value(), GetTextElement(parameter));
478 }
479
480 return true;
481 }
482
483 namespace {
484 struct xml_memory_writer : pugi::xml_writer
485 {
486 size_t written{};
487 char* buffer{};
488 size_t remaining{};
489
write__anon242722de0111::xml_memory_writer490 virtual void write(void const* data, size_t size)
491 {
492 if (buffer && size <= remaining) {
493 memcpy(buffer, data, size);
494 buffer += size;
495 remaining -= size;
496 }
497 written += size;
498 }
499 };
500 }
501
GetRawDataLength()502 size_t CXmlFile::GetRawDataLength()
503 {
504 if (!m_document) {
505 return 0;
506 }
507
508 xml_memory_writer writer;
509 m_document.save(writer);
510 return writer.written;
511 }
512
GetRawDataHere(char * p,size_t size)513 void CXmlFile::GetRawDataHere(char* p, size_t size) // p has to big enough to hold at least GetRawDataLength() bytes
514 {
515 if (size) {
516 memset(p, 0, size);
517 }
518 xml_memory_writer writer;
519 writer.buffer = p;
520 writer.remaining = size;
521 m_document.save(writer);
522 }
523
ParseData(uint8_t const * data,size_t len)524 bool CXmlFile::ParseData(uint8_t const* data, size_t len)
525 {
526 Close();
527 m_document.load_buffer(data, len);
528 m_element = m_document.child(m_rootName.c_str());
529 if (!m_element) {
530 Close();
531 }
532 return !!m_element;
533 }
534
IsFromFutureVersion() const535 bool CXmlFile::IsFromFutureVersion() const
536 {
537 auto const ownVer = GetFileZillaVersion();
538 if (!m_element || ownVer.empty()) {
539 return false;
540 }
541 std::wstring const version = GetTextAttribute(m_element, "version");
542 return ConvertToVersionNumber(ownVer.c_str()) < ConvertToVersionNumber(version.c_str());
543 }
544