#include "webdav.hpp" #include #include #include #include "curl/curl.h" #include "curl/easy.h" #include using namespace std; using namespace tinyxml2; WebDavClient::WebDavClient(string w, string l) : web_root(w), local_root(l) { curl = curl_easy_init(); reset(); } WebDavClient::~WebDavClient() { curl_easy_cleanup(this->curl); } void WebDavClient::reset() { curl_easy_reset(this->curl); curl_easy_setopt(this->curl, CURLOPT_USERAGENT, "3DavSync 0.1.0"); curl_easy_setopt(this->curl, CURLOPT_CONNECTTIMEOUT, 50L); curl_easy_setopt(this->curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(this->curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 1L); curl_easy_setopt(this->curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(this->curl, CURLOPT_PIPEWAIT, 1L); // curl_easy_setopt(this->curl, CURLOPT_VERBOSE, 1L); if (this->use_basic_auth) { curl_easy_setopt(this->curl, CURLOPT_USERNAME, this->username.c_str()); curl_easy_setopt(this->curl, CURLOPT_PASSWORD, this->password.c_str()); curl_easy_setopt(this->curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); } } void WebDavClient::set_basic_auth(std::string username, std::string password) { this->username = username; this->password = password; this->use_basic_auth = true; this->reset(); } string formulate_actual_url(string &root, string &rel_path) { if (!rel_path.empty()) { string escaped_string = curl_easy_escape(NULL, rel_path.c_str(), 0); string escaped_string_with_slash = regex_replace(escaped_string, regex("%2F"), "/"); string res = root + escaped_string_with_slash; return res; } else { return root; } } bool WebDavClient::mkcol(string web_rel_path, optional mtime) { string actual_url = formulate_actual_url(this->web_root, web_rel_path); // Test if directory existed already curl_easy_setopt(this->curl, CURLOPT_URL, actual_url.c_str()); curl_easy_setopt(this->curl, CURLOPT_NOBODY, 1L); CURLcode head_res = curl_easy_perform(this->curl); if (head_res == CURLE_OK) { this->reset(); return true; } this->reset(); curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, "MKCOL"); curl_easy_setopt(this->curl, CURLOPT_URL, actual_url.c_str()); if (mtime) { u64 actual_mtime = mtime.value(); struct curl_slist *list = NULL; string oc_mtime = string("X-OC-Mtime: " + to_string(actual_mtime)).c_str(); list = curl_slist_append(list, oc_mtime.c_str()); curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, list); } CURLcode curl_res = curl_easy_perform(this->curl); if (curl_res != CURLE_OK) { long response_code; curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); printf("curl MKCOL failed: %s (%d)\n", curl_easy_strerror(curl_res), curl_res); printf("Location: %s\n", actual_url.c_str()); printf("Response code: %ld\n", response_code); consoleUpdate(NULL); this->reset(); return false; } else { this->reset(); return true; } } bool WebDavClient::pull(string path, string web_rel_path) { const char *c_path = path.c_str(); if (this->curl) { // Formulate and fill actual url string actual_url = formulate_actual_url(this->web_root, web_rel_path); curl_easy_setopt(curl, CURLOPT_URL, actual_url.c_str()); // Open a file at that path, overwrite if exists FILE *fp = fopen(c_path, "w"); if (fp) { curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, fp); CURLcode res = curl_easy_perform(curl); fclose(fp); if (res != 0) { long response_code; curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); printf("error getting file %s: %s\n", c_path, curl_easy_strerror(res)); printf("Location: %s\n", actual_url.c_str()); printf("Response code: %ld\n", response_code); consoleUpdate(NULL); this->reset(); return false; } } else { printf("can't open %s for writing: %s\n", c_path, strerror(errno)); consoleUpdate(NULL); } } else { printf("can't initalize curl!\n"); consoleUpdate(NULL); } this->reset(); return true; } static int seek_helper(void *userp, curl_off_t offset, int origin) { FILE *fp = (FILE *)userp; if (-1 == fseek(fp, (long)offset, origin)) /* couldn't seek */ return CURL_SEEKFUNC_CANTSEEK; return CURL_SEEKFUNC_OK; /* success! */ } bool WebDavClient::push(string path, string web_rel_path) { const char *c_path = path.c_str(); if (this->curl) { // Formulate and fill actual url string actual_url = formulate_actual_url(this->web_root, web_rel_path); curl_easy_setopt(curl, CURLOPT_URL, actual_url.c_str()); // Open a file at that path to read FILE *fp = fopen(c_path, "r"); if (fp) { // Read file metadata struct stat file_info; fstat(fileno(fp), &file_info); // Set curl reading stuff curl_easy_setopt(this->curl, CURLOPT_READDATA, fp); curl_easy_setopt(this->curl, CURLOPT_SEEKFUNCTION, seek_helper); // Prepare the headers // For Nextcloud/ownCloud, we can ask the server to use our mtime u64 mtime; struct stat attr; stat(path.c_str(), &attr); mtime = attr.st_mtime; struct curl_slist *list = NULL; string t = string("X-OC-Mtime: " + to_string(mtime)).c_str(); list = curl_slist_append(list, t.c_str()); curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, list); // We are uploading! curl_easy_setopt(this->curl, CURLOPT_UPLOAD, 1L); curl_easy_setopt(this->curl, CURLOPT_INFILESIZE, (curl_off_t)file_info.st_size); CURLcode res = curl_easy_perform(curl); fclose(fp); if (res != CURLE_OK) { long response_code; curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); printf("error pushing file %s: %s\n", c_path, curl_easy_strerror(res)); printf("Location: %s\n", actual_url.c_str()); printf("Response code: %ld\n", response_code); consoleUpdate(NULL); this->reset(); return false; } } else { printf("can't open %s for writing: %s\n", c_path, strerror(errno)); consoleUpdate(NULL); } } else { printf("can't initalize curl!\n"); consoleUpdate(NULL); } this->reset(); return true; } /// Write the curl response to a std::string size_t curl_write_to_string(void *ptr, size_t size, size_t nmemb, string *s) { s->append(static_cast(ptr), size * nmemb); return size * nmemb; } optional> normalize_filelist(optional> i) { if (i) { auto files = i.value(); if (files.empty()) { return nullopt; } else if (files.size() == 1) { // Only contains the root directory, we don't care about that files.erase(files.begin()); } else if (files.size() > 1) { // Seems to be a query on collection // Get root first if (files[0].path.back() == '/') { string root = files[0].path.c_str(); root.pop_back(); for (FileEntry &file : files) { file.path.erase(0, root.length()); } } } return files; } else { return nullopt; } } const char *query = R"( )"; /// Read the timestamp from WebDAV server optional> WebDavClient::get_remote_files() { // Use PROPFIND to fetch file metadata if (this->curl) { string response = ""; // Formulate and fill url curl_easy_setopt(curl, CURLOPT_URL, this->web_root.c_str()); // Read XML content curl_easy_setopt(this->curl, CURLOPT_CUSTOMREQUEST, "PROPFIND"); curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, curl_write_to_string); curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, &response); curl_easy_setopt(this->curl, CURLOPT_POSTFIELDS, query); struct curl_slist *list = NULL; list = curl_slist_append(list, "Depth: infinity"); curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, list); CURLcode curl_res = curl_easy_perform(this->curl); if (curl_res != CURLE_OK) { long response_code; curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &response_code); printf("curl PROPFIND failed: %s (%d)\n", curl_easy_strerror(curl_res), curl_res); printf("Response code: %ld\n", response_code); consoleUpdate(NULL); this->reset(); return nullopt; } this->reset(); // Now read it! XMLDocument doc; XMLError parse_res = doc.Parse(response.c_str()); vector res; if (parse_res == tinyxml2::XML_SUCCESS) { XMLElement *root = doc.RootElement(); for (XMLElement *e = root->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { string path; time_t time; bool folder = false; int size; // Get file path if (e->FirstChildElement("d:href")) { const char *text = e->FirstChildElement("d:href")->GetText(); // The path here is escaped. Convert them back to unescaped form char *decoded = curl_easy_unescape(this->curl, text, 0, NULL); path = string(decoded); } else { printf("malformed WebDAV response: missing d:href in PROPFIND\n"); consoleUpdate(NULL); return nullopt; } if (e->FirstChildElement("d:propstat")) { if (e->FirstChildElement("d:propstat")->FirstChildElement("d:prop")) { XMLElement *prop = e->FirstChildElement("d:propstat")->FirstChildElement("d:prop"); if (prop->FirstChildElement("d:getlastmodified")) { string time_str = prop->FirstChildElement("d:getlastmodified")->GetText(); time = curl_getdate(time_str.c_str(), NULL); } else { printf("malformed WebDAV response: missing d:getlastmodified in PROPFIND\n"); consoleUpdate(NULL); return nullopt; } if (prop->FirstChildElement("d:getcontentlength")) { string size_str = prop->FirstChildElement("d:getcontentlength")->GetText(); size = stoi(size_str); } else { size = 0; } if (prop->FirstChildElement("d:resourcetype") && prop->FirstChildElement("d:resourcetype")->FirstChildElement("d:collection")) { folder = true; } } else { printf("malformed WebDAV response: missing d:prop in PROPFIND\n"); consoleUpdate(NULL); return nullopt; } } else { printf("malformed WebDAV response: missing d:propstat in PROPFIND\n"); consoleUpdate(NULL); return nullopt; } res.push_back(FileEntry{path, time, folder, size}); } } else { printf("malformed XML response: %d\n", parse_res); consoleUpdate(NULL); return nullopt; } // Turn file paths into canonical form auto out = normalize_filelist(res); return out; } else { printf("can't initalize curl!\n"); consoleUpdate(NULL); return nullopt; } } optional get_file(vector *files, string path) { for (unsigned i = 0; i < files->size(); i++) { if ((*files)[i].path == path) { FileEntry f = (*files)[i]; files->erase(files->begin() + i); return f; } } return nullopt; } vector> recursively_get_dir(string base_path, string ext_path = "") { vector> paths; DIR *dir; struct dirent *ent; string path = base_path + ext_path; if ((dir = opendir(path.c_str())) != NULL) { paths.push_back(pair(ext_path + "/", true)); while ((ent = readdir(dir)) != NULL) { vector> subdir = recursively_get_dir(base_path, ext_path + "/" + ent->d_name); paths.insert(paths.end(), subdir.begin(), subdir.end()); } } else { if (ext_path != "") { paths.push_back(make_pair(ext_path, false)); } else { printf("directory %s not found\n", path.c_str()); consoleUpdate(NULL); } } closedir(dir); return paths; } bool user_confirm() { PadState pad; while (appletMainLoop()) { padUpdate(&pad); u32 kDown = padGetButtonsDown(&pad); if (kDown & HidNpadButton_A) { return true; } else if (kDown & HidNpadButton_B) { return false; } } return false; } bool WebDavClient::compareAndUpdate() { bool success = true; struct stat rootstat; if (!stat(this->local_root.c_str(), &rootstat)) { // Create directory mkdir(this->local_root.c_str(), 0777); } else if (!S_ISDIR(rootstat.st_mode)) { printf(CONSOLE_RED "specified local dir is not a dir!" CONSOLE_RESET); consoleUpdate(NULL); return false; } this->mkcol("", nullopt); optional> remote_files_optional = this->get_remote_files(); if (!remote_files_optional) { printf("failed to fetch remote file list\n"); consoleUpdate(NULL); return false; } vector remote_files = remote_files_optional.value(); vector> local_files = recursively_get_dir(this->local_root); for (auto [path, is_dir] : local_files) { string local_real_path = this->local_root + path; time_t local_mtime; struct stat attr; stat(local_real_path.c_str(), &attr); local_mtime = attr.st_mtime; // fsdev_getmtime(local_real_path.c_str(), &local_mtime); optional remote_file_optional = get_file(&remote_files, path); if (remote_file_optional) { FileEntry remote_file = remote_file_optional.value(); if (is_dir) { // Local && Remote directory exists, we are fine } else if (local_mtime > remote_file.last_modified) { // Ask if we should do an upload printf("\n%s", path.c_str()); printf(CONSOLE_YELLOW "Local version newer on above file.\n" CONSOLE_RESET); printf(CONSOLE_YELLOW "Upload (A) or Not (B)?\n" CONSOLE_RESET); consoleUpdate(NULL); if (user_confirm()) { // Upload local version printf("%s: local modified, uploading...\n\n", path.c_str()); consoleUpdate(NULL); if (!this->push(local_real_path, path)) { printf(CONSOLE_RED "%s: local modified, upload failed.\n", path.c_str()); printf(CONSOLE_RESET); consoleUpdate(NULL); success = false; } } } else if (local_mtime < remote_file.last_modified) { // Ask if we should do an upload printf("\n%s", path.c_str()); printf(CONSOLE_YELLOW "Local version older on above file.\n" CONSOLE_RESET); printf(CONSOLE_YELLOW "Download (A) or Not (B)?\n" CONSOLE_RESET); consoleUpdate(NULL); // Pull remote version if (user_confirm()) { printf("%s: remote modified, downloading...\n\n", remote_file.path.c_str()); consoleUpdate(NULL); if (!this->pull(local_real_path, remote_file.path)) { printf(CONSOLE_RED "%s: remote modified, download failed.\n", path.c_str()); printf(CONSOLE_RESET); consoleUpdate(NULL); success = false; } } } else { // Identical, don't do anything } } else { // Remote file DNE, upload local version if (is_dir) { this->mkcol(path, local_mtime); } else { printf("%s: new local file, uploading...\n\n", path.c_str()); consoleUpdate(NULL); if (!this->push(local_real_path, path)) { printf(CONSOLE_RED "%s: upload failed.\n\n", path.c_str()); printf(CONSOLE_RESET); consoleUpdate(NULL); success = false; } else { } } } } // Pull the remaining remote files for (FileEntry remote_file : remote_files) { string real_local_path = this->local_root + remote_file.path; if (remote_file.path == "/") { continue; } else if (remote_file.folder) { int status = mkdir(real_local_path.c_str(), 0777); if (status != 0) { printf("can't create local dir %s: %s\n", real_local_path.c_str(), strerror(errno)); consoleUpdate(NULL); success = false; } } else { // It's a file printf("%s: new remote file, downloading...\n\n", remote_file.path.c_str()); consoleUpdate(NULL); if (!this->pull(real_local_path, remote_file.path)) { printf("can't download remote file %s\n", remote_file.path.c_str()); consoleUpdate(NULL); success = false; } else { } } } return success; }