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