/* GIO - GLib Input, Output and Streaming Library * * Copyright (C) 2008 Red Hat, Inc. * Copyright (C) 2021 Igalia S.L. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * Author: Christian Kellner */ #include #include #include #include #include #include #include #include #include #include #include /* LibXML2 includes */ #include #include #include #include #include "gvfsbackenddav.h" #include "gvfskeyring.h" #include "gvfsjobmount.h" #include "gvfsjobopenforread.h" #include "gvfsjobread.h" #include "gvfsjobseekread.h" #include "gvfsjobopenforwrite.h" #include "gvfsjobwrite.h" #include "gvfsjobseekwrite.h" #include "gvfsjobsetdisplayname.h" #include "gvfsjobqueryinfo.h" #include "gvfsjobqueryfsinfo.h" #include "gvfsjobqueryattributes.h" #include "gvfsjobenumerate.h" #include "gvfsjobpush.h" #include "gvfsdaemonprotocol.h" #include "gvfsdaemonutils.h" #include "gvfsutils.h" #ifdef HAVE_AVAHI #include "gvfsdnssdutils.h" #include "gvfsdnssdresolver.h" #endif typedef struct _MountAuthData MountAuthData; static void mount_auth_info_free (MountAuthData *info); #ifdef HAVE_AVAHI static void dns_sd_resolver_changed (GVfsDnsSdResolver *resolver, GVfsBackendDav *dav_backend); #endif static gboolean soup_authenticate (SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data); typedef struct _AuthInfo { /* for server authentication */ char *username; char *password; char *realm; GPasswordSave pw_save; } AuthInfo; struct _MountAuthData { SoupSession *session; GMountSource *mount_source; gboolean retrying_after_403; gboolean interactive; AuthInfo server_auth; AuthInfo proxy_auth; }; struct _GVfsBackendDav { GVfsBackendHttp parent_instance; MountAuthData auth_info; gchar *last_good_path; /* Used for user-verified secure connections. */ GTlsCertificate *certificate; GTlsCertificateFlags certificate_errors; #ifdef HAVE_AVAHI /* only set if we're handling a [dav|davs]+sd:// mounts */ GVfsDnsSdResolver *resolver; #endif }; G_DEFINE_TYPE (GVfsBackendDav, g_vfs_backend_dav, G_VFS_TYPE_BACKEND_HTTP); static void g_vfs_backend_dav_finalize (GObject *object) { GVfsBackendDav *dav_backend; dav_backend = G_VFS_BACKEND_DAV (object); #ifdef HAVE_AVAHI if (dav_backend->resolver != NULL) { g_signal_handlers_disconnect_by_func (dav_backend->resolver, dns_sd_resolver_changed, dav_backend); g_object_unref (dav_backend->resolver); } #endif mount_auth_info_free (&(dav_backend->auth_info)); g_clear_object (&dav_backend->certificate); if (G_OBJECT_CLASS (g_vfs_backend_dav_parent_class)->finalize) (*G_OBJECT_CLASS (g_vfs_backend_dav_parent_class)->finalize) (object); } static void g_vfs_backend_dav_init (GVfsBackendDav *backend) { g_vfs_backend_set_user_visible (G_VFS_BACKEND (backend), TRUE); } /* ************************************************************************* */ /* Small utility functions */ static gboolean string_to_uint64 (const char *str, guint64 *value) { char *endptr; *value = g_ascii_strtoull (str, &endptr, 10); return endptr != str; } static inline gboolean sm_has_header (SoupMessage *msg, const char *header) { return soup_message_headers_get_one (soup_message_get_response_headers (msg), header) != NULL; } static char * path_get_parent_dir (const char *path) { char *parent; size_t len; len = strlen (path); while (len > 0 && path[len - 1] == '/') len--; if (len == 0) return g_strdup ("/"); parent = g_strrstr_len (path, len, "/"); if (parent == NULL) return g_strdup ("/"); return g_strndup (path, (parent - path) + 1); } /* message utility functions */ static void message_add_destination_header (SoupMessage *msg, GUri *uri) { char *string; string = g_uri_to_string_partial (uri, G_URI_HIDE_PASSWORD); soup_message_headers_append (soup_message_get_request_headers (msg), "Destination", string); g_free (string); } static void message_add_overwrite_header (SoupMessage *msg, gboolean overwrite) { soup_message_headers_append (soup_message_get_request_headers (msg), "Overwrite", overwrite ? "T" : "F"); } static void message_add_redirect_header (SoupMessage *msg, GFileQueryInfoFlags flags) { const char *header_redirect; /* RFC 4437 */ if (flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS) header_redirect = "F"; else header_redirect = "T"; soup_message_headers_append (soup_message_get_request_headers (msg), "Apply-To-Redirect-Ref", header_redirect); } static inline gboolean str_equal (const char *a, const char *b, gboolean insensitive) { if (a == NULL || b == NULL) return a == b; return insensitive ? !g_ascii_strcasecmp (a, b) : !strcmp (a, b); } static gboolean path_equal (const char *a, const char *b, gboolean relax) { gboolean res; size_t a_len, b_len; if (relax == FALSE) return str_equal (a, b, FALSE); if (a == NULL || b == NULL) return a == b; a_len = strlen (a); b_len = strlen (b); while (a_len > 0 && a[a_len - 1] == '/') a_len--; while (b_len > 0 && b[b_len - 1] == '/') b_len--; if (a_len == b_len) res = ! strncmp (a, b, a_len); else res = FALSE; return res; } static gboolean dav_uri_match (GUri *a, GUri *b, gboolean relax) { gboolean diff; char *ua, *ub; ua = g_uri_unescape_string (g_uri_get_path (a), "/"); ub = g_uri_unescape_string (g_uri_get_path (b), "/"); diff = g_uri_get_port (a) != g_uri_get_port (b) || !str_equal (g_uri_get_scheme (a), g_uri_get_scheme (b), FALSE) || !str_equal (g_uri_get_host (a), g_uri_get_host (b), TRUE) || !path_equal (ua, ub, relax) || !str_equal (g_uri_get_query (a), g_uri_get_query (b), FALSE) || !str_equal (g_uri_get_fragment (a), g_uri_get_fragment (b), FALSE); g_free (ua); g_free (ub); return !diff; } static char * dav_uri_encode (const char *path_to_encode) { char *path; static const char *allowed_reserved_chars = "/"; path = g_uri_escape_string (path_to_encode, allowed_reserved_chars, FALSE); return path; } static gboolean message_should_apply_redir_ref (SoupMessage *msg) { const char *header; header = soup_message_headers_get_one (soup_message_get_request_headers (msg), "Apply-To-Redirect-Ref"); if (header == NULL || g_ascii_strcasecmp (header, "T")) return FALSE; return TRUE; } static GUri * g_vfs_backend_dav_uri_for_path (GVfsBackend *backend, const char *path, gboolean is_dir) { GUri *mount_base; GUri *uri; char *fn_encoded; char *new_path; mount_base = http_backend_get_mount_base (backend); /* "/" means "whatever mount_base is" */ if (!strcmp (path, "/")) return g_uri_ref (mount_base); /* The mount_base path is escaped already so we need to escape the new path as well */ fn_encoded = dav_uri_encode (path); /* Otherwise, we append filename to mount_base (which is assumed to * be a directory in this case). * * Add a "/" in cases where it is likely that the url is going * to be a directory to avoid redirections */ if (is_dir == FALSE || g_str_has_suffix (path, "/")) new_path = g_build_path ("/", g_uri_get_path (mount_base), fn_encoded, NULL); else new_path = g_build_path ("/", g_uri_get_path (mount_base), fn_encoded, "/", NULL); uri = soup_uri_copy (mount_base, SOUP_URI_PATH, new_path, SOUP_URI_NONE); g_free (fn_encoded); g_free (new_path); return uri; } static gboolean g_vfs_backend_dav_stream_skip (GInputStream *stream, GError **error) { for (;;) { gssize skipped = g_input_stream_skip (stream, 4096, NULL, error); if (skipped < 0) { return FALSE; } else if (!skipped) break; } g_input_stream_close (stream, NULL, NULL); return TRUE; } static void g_vfs_backend_dav_setup_display_name (GVfsBackend *backend) { GUri *mount_base; char *display_name; char port[7] = {0, }; gint gport; #ifdef HAVE_AVAHI GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); if (dav_backend->resolver != NULL) { const char *name; name = g_vfs_dns_sd_resolver_get_service_name (dav_backend->resolver); g_vfs_backend_set_display_name (backend, name); return; } #endif mount_base = http_backend_get_mount_base (backend); gport = g_uri_get_port (mount_base); if ((gport > 0) && (gport != 80) && (gport != 443)) g_snprintf (port, sizeof (port), ":%u", g_uri_get_port (mount_base)); if (g_uri_get_user (mount_base) != NULL) /* Translators: This is the name of the WebDAV share constructed as "WebDAV as on :"; the ":" part is the second %s and only shown if it is not the default http(s) port. */ display_name = g_strdup_printf (_("%s on %s%s"), g_uri_get_user (mount_base), g_uri_get_host (mount_base), port); else display_name = g_strdup_printf ("%s%s", g_uri_get_host (mount_base), port); g_vfs_backend_set_display_name (backend, display_name); g_free (display_name); } static gboolean accept_certificate (SoupMessage *msg, GTlsCertificate *certificate, GTlsCertificateFlags errors, gpointer user_data) { GVfsBackendDav *dav = G_VFS_BACKEND_DAV (user_data); return (errors == dav->certificate_errors && g_tls_certificate_is_same (certificate, dav->certificate)); } static void dav_message_connect_signals (SoupMessage *message, GVfsBackend *backend) { GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); /* we always have to connect this as the message can be * re-sent with a differently set certificate_errors field */ g_signal_connect (message, "accept-certificate", G_CALLBACK (accept_certificate), backend); g_signal_connect (message, "authenticate", G_CALLBACK (soup_authenticate), &dav_backend->auth_info); } static void dav_send_async_with_redir_cb (GObject *source, GAsyncResult *ret, gpointer user_data) { SoupSession *session = SOUP_SESSION (source); SoupMessage *msg = soup_session_get_async_result_message (session, ret); GInputStream *body; GError *error = NULL; GTask *task = user_data; const char *new_loc; GUri *new_uri; GUri *old_uri; GUri *tmp; guint status; gboolean redirect; body = soup_session_send_finish (session, ret, &error); if (!body) goto return_error; status = soup_message_get_status (msg); if (!SOUP_STATUS_IS_REDIRECTION (status)) goto return_body; new_loc = soup_message_headers_get_one (soup_message_get_response_headers (msg), "Location"); if (new_loc == NULL) goto return_body; old_uri = soup_message_get_uri (msg); new_uri = g_uri_parse_relative (old_uri, new_loc, SOUP_HTTP_URI_FLAGS, &error); if (new_uri == NULL) { g_object_unref (body); goto return_error; } tmp = new_uri; new_uri = soup_uri_copy (new_uri, SOUP_URI_USER, g_uri_get_user (old_uri), SOUP_URI_AUTH_PARAMS, g_uri_get_auth_params (old_uri), SOUP_URI_NONE); g_uri_unref (tmp); /* Check if this is a trailing slash redirect (i.e. /a/b to /a/b/), * redirect it right away */ redirect = dav_uri_match (new_uri, old_uri, TRUE); if (redirect) { const char *dest; dest = soup_message_headers_get_one (soup_message_get_request_headers (msg), "Destination"); if (dest && g_str_has_suffix (dest, "/") == FALSE) { char *new_dest = g_strconcat (dest, "/", NULL); soup_message_headers_replace (soup_message_get_request_headers (msg), "Destination", new_dest); g_free (new_dest); } } else if (message_should_apply_redir_ref (msg)) { if (status == SOUP_STATUS_MOVED_PERMANENTLY || status == SOUP_STATUS_TEMPORARY_REDIRECT) { const char *method = soup_message_get_method (msg); /* Only cross-site redirect safe methods */ if (method == SOUP_METHOD_GET && method == SOUP_METHOD_HEAD && method == SOUP_METHOD_OPTIONS && method == SOUP_METHOD_PROPFIND) redirect = TRUE; } /* Two possibilities: * * 1) It's a non-redirecty 3xx response (300, 304, * 305, 306) * 2) It's some newly-defined 3xx response (308+) * * We ignore both of these cases. In the first, * redirecting would be explicitly wrong, and in the * last case, we have no clue if the 3xx response is * supposed to be redirecty or non-redirecty. Plus, * 2616 says unrecognized status codes should be * treated as the equivalent to the x00 code, and we * don't redirect on 300, so therefore we shouldn't * redirect on 308+ either. */ } if (!redirect) { g_uri_unref (new_uri); goto return_body; } if (!g_vfs_backend_dav_stream_skip (body, &error)) { g_object_unref (body); goto return_error; } g_object_unref (body); soup_message_set_uri (msg, new_uri); g_uri_unref (new_uri); /* recurse */ soup_session_send_async (session, msg, G_PRIORITY_DEFAULT, NULL, dav_send_async_with_redir_cb, g_object_ref (task)); goto return_done; return_body: g_task_return_pointer (task, body, g_object_unref); goto return_done; return_error: g_task_return_error (task, error); return_done: g_object_unref (task); } static void g_vfs_backend_dav_send_async (GVfsBackend *backend, SoupMessage *message, GAsyncReadyCallback callback, gpointer user_data) { SoupSession *session = G_VFS_BACKEND_HTTP (backend)->session; GTask *task = g_task_new (backend, NULL, callback, user_data); g_task_set_source_tag (task, g_vfs_backend_dav_send_async); g_task_set_task_data (task, g_object_ref (message), g_object_unref); soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT); soup_session_send_async (session, message, G_PRIORITY_DEFAULT, NULL, dav_send_async_with_redir_cb, task); } static GInputStream * g_vfs_backend_dav_send_finish (GVfsBackend *backend, GAsyncResult *result, GError **error) { g_return_val_if_fail (G_VFS_IS_BACKEND (backend), NULL); g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); g_return_val_if_fail (g_task_is_valid (result, backend), NULL); g_return_val_if_fail (g_async_result_is_tagged (result, g_vfs_backend_dav_send_async), NULL); return g_task_propagate_pointer (G_TASK (result), error); } static SoupMessage * g_vfs_backend_dav_get_async_result_message (GVfsBackend *backend, GAsyncResult *result) { g_return_val_if_fail (G_VFS_IS_BACKEND (backend), NULL); g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL); g_return_val_if_fail (g_task_is_valid (result, backend), NULL); return g_task_get_task_data (G_TASK (result)); } /* ************************************************************************* */ /* generic xml parsing functions */ static inline gboolean node_has_name (xmlNodePtr node, const char *name) { g_return_val_if_fail (node != NULL, FALSE); return ! strcmp ((char *) node->name, name); } static inline gboolean node_has_name_ns (xmlNodePtr node, const char *name, const char *ns_href) { gboolean has_name; gboolean has_ns; g_return_val_if_fail (node != NULL, FALSE); has_name = has_ns = TRUE; if (name) has_name = node->name && ! strcmp ((char *) node->name, name); if (ns_href) has_ns = node->ns && node->ns->href && ! g_ascii_strcasecmp ((char *) node->ns->href, ns_href); return has_name && has_ns; } static inline gboolean node_is_element (xmlNodePtr node) { return node->type == XML_ELEMENT_NODE && node->name != NULL; } static inline gboolean node_is_element_with_name (xmlNodePtr node, const char *name) { return node->type == XML_ELEMENT_NODE && node->name != NULL && ! strcmp ((char *) node->name, name); } static const char * node_get_content (xmlNodePtr node) { if (node == NULL) return NULL; switch (node->type) { case XML_ELEMENT_NODE: return node_get_content (node->children); break; case XML_TEXT_NODE: return (const char *) node->content; break; default: return NULL; } } static gboolean node_is_empty (xmlNodePtr node) { if (node == NULL) return TRUE; if (node->type == XML_TEXT_NODE) return node->content == NULL || node->content[0] == '\0'; return node->children == NULL; } typedef struct _xmlNodeIter { xmlNodePtr cur_node; xmlNodePtr next_node; const char *name; const char *ns_href; void *user_data; } xmlNodeIter; static xmlNodePtr xml_node_iter_next (xmlNodeIter *iter) { xmlNodePtr node; while ((node = iter->next_node)) { iter->next_node = node->next; if (node->type == XML_ELEMENT_NODE) { if (node_has_name_ns (node, iter->name, iter->ns_href)) break; } } iter->cur_node = node; return node; } static void * xml_node_iter_get_user_data (xmlNodeIter *iter) { return iter->user_data; } static xmlNodePtr xml_node_iter_get_current (xmlNodeIter *iter) { return iter->cur_node; } static int xml_read_cb (void *ctx, char *buf, int len) { return g_input_stream_read (ctx, buf, len, NULL, NULL); } static xmlDocPtr parse_xml (SoupMessage *msg, GInputStream *body, xmlNodePtr *root, const char *name, GError **error) { xmlDocPtr doc; if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) { g_set_error (error, G_IO_ERROR, http_error_code_from_status (soup_message_get_status (msg)), _("HTTP Error: %s"), soup_message_get_reason_phrase (msg)); return NULL; } doc = xmlReadIO (xml_read_cb, NULL, body, "response.xml", NULL, XML_PARSE_NONET | XML_PARSE_NOWARNING | XML_PARSE_NOBLANKS | XML_PARSE_NSCLEAN | XML_PARSE_NOCDATA | XML_PARSE_COMPACT); if (doc == NULL) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Could not parse response")); return NULL; } *root = xmlDocGetRootElement (doc); if (*root == NULL || (*root)->children == NULL) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Empty response")); xmlFreeDoc (doc); return NULL; } if (strcmp ((char *) (*root)->name, name)) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Unexpected reply from server")); xmlFreeDoc (doc); return NULL; } return doc; } /* ************************************************************************* */ /* Multistatus parsing code */ typedef struct _Multistatus Multistatus; typedef struct _MsResponse MsResponse; typedef struct _MsPropstat MsPropstat; struct _Multistatus { xmlDocPtr doc; xmlNodePtr root; GUri *target; char *path; }; struct _MsResponse { Multistatus *multistatus; char *path; gboolean is_target; xmlNodePtr first_propstat; }; struct _MsPropstat { Multistatus *multistatus; xmlNodePtr prop_node; guint status_code; }; static gboolean multistatus_parse (SoupMessage *msg, GInputStream *body, Multistatus *multistatus, GError **error) { xmlDocPtr doc; xmlNodePtr root; GUri *uri; doc = parse_xml (msg, body, &root, "multistatus", error); if (doc == NULL) return FALSE; uri = soup_message_get_uri (msg); multistatus->doc = doc; multistatus->root = root; multistatus->target = uri; multistatus->path = g_uri_unescape_string (g_uri_get_path (uri), "/"); return TRUE; } static void multistatus_free (Multistatus *multistatus) { xmlFreeDoc (multistatus->doc); g_free (multistatus->path); } static void multistatus_get_response_iter (Multistatus *multistatus, xmlNodeIter *iter) { iter->cur_node = multistatus->root->children; iter->next_node = multistatus->root->children; iter->name = "response"; iter->ns_href = "DAV:"; iter->user_data = multistatus; } static gboolean multistatus_get_response (xmlNodeIter *resp_iter, MsResponse *response) { Multistatus *multistatus; xmlNodePtr resp_node; xmlNodePtr iter; xmlNodePtr href; xmlNodePtr propstat; GUri *uri; const char *text; char *path; multistatus = xml_node_iter_get_user_data (resp_iter); resp_node = xml_node_iter_get_current (resp_iter); if (resp_node == NULL) return FALSE; propstat = NULL; href = NULL; for (iter = resp_node->children; iter; iter = iter->next) { if (! node_is_element (iter)) { continue; } else if (node_has_name_ns (iter, "href", "DAV:")) { href = iter; } else if (node_has_name_ns (iter, "propstat", "DAV:")) { if (propstat == NULL) propstat = iter; } if (href && propstat) break; } if (href == NULL) return FALSE; text = node_get_content (href); if (text == NULL) return FALSE; uri = g_uri_parse_relative (multistatus->target, text, SOUP_HTTP_URI_FLAGS, NULL); if (uri == NULL) return FALSE; path = g_uri_unescape_string (g_uri_get_path (uri), "/"); g_uri_unref (uri); response->path = path; response->is_target = path_equal (path, multistatus->path, TRUE); response->multistatus = multistatus; response->first_propstat = propstat; return resp_node != NULL; } static void ms_response_clear (MsResponse *response) { g_free (response->path); } static char * ms_response_get_basename (MsResponse *response) { return http_path_get_basename (response->path); } static void ms_response_get_propstat_iter (MsResponse *response, xmlNodeIter *iter) { iter->cur_node = response->first_propstat; iter->next_node = response->first_propstat; iter->name = "propstat"; iter->ns_href = "DAV:"; iter->user_data = response; } static guint ms_response_get_propstat (xmlNodeIter *cur_node, MsPropstat *propstat) { MsResponse *response; xmlNodePtr pstat_node; xmlNodePtr iter; xmlNodePtr prop; xmlNodePtr status; const char *status_text; gboolean res; guint code; response = xml_node_iter_get_user_data (cur_node); pstat_node = xml_node_iter_get_current (cur_node); if (pstat_node == NULL) return 0; status = NULL; prop = NULL; for (iter = pstat_node->children; iter; iter = iter->next) { if (!node_is_element (iter)) { continue; } else if (node_has_name_ns (iter, "status", "DAV:")) { status = iter; } else if (node_has_name_ns (iter, "prop", "DAV:")) { prop = iter; } if (status && prop) break; } status_text = node_get_content (status); if (status_text == NULL || prop == NULL) return 0; res = soup_headers_parse_status_line ((char *) status_text, NULL, &code, NULL); if (res == FALSE) return 0; propstat->prop_node = prop; propstat->status_code = code; propstat->multistatus = response->multistatus; return code; } static GFileType parse_resourcetype (xmlNodePtr rt) { xmlNodePtr node; GFileType type; for (node = rt->children; node; node = node->next) { if (node_is_element (node)) break; } if (node == NULL) return G_FILE_TYPE_REGULAR; if (! strcmp ((char *) node->name, "collection")) type = G_FILE_TYPE_DIRECTORY; else if (! strcmp ((char *) node->name, "redirectref")) type = G_FILE_TYPE_SYMBOLIC_LINK; else type = G_FILE_TYPE_UNKNOWN; return type; } static inline void file_info_set_content_type (GFileInfo *info, const char *type, gboolean uncertain_content_type) { if (!uncertain_content_type) g_file_info_set_content_type (info, type); g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, type); } static void ms_response_to_file_info (MsResponse *response, GFileInfo *info) { xmlNodeIter iter; MsPropstat propstat; xmlNodePtr node; guint status; char *basename; const char *text; GFileType file_type; char *mime_type; gboolean uncertain_content_type; GIcon *icon; GIcon *symbolic_icon; gboolean have_display_name; basename = ms_response_get_basename (response); g_file_info_set_name (info, basename); g_file_info_set_edit_name (info, basename); if (basename && basename[0] == '.') g_file_info_set_is_hidden (info, TRUE); g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE); file_type = G_FILE_TYPE_REGULAR; mime_type = NULL; uncertain_content_type = FALSE; have_display_name = FALSE; ms_response_get_propstat_iter (response, &iter); while (xml_node_iter_next (&iter)) { status = ms_response_get_propstat (&iter, &propstat); if (! SOUP_STATUS_IS_SUCCESSFUL (status)) continue; for (node = propstat.prop_node->children; node; node = node->next) { if (! node_is_element (node) || node_is_empty (node)) continue; /* TODO: check namespace, parse user data nodes*/ text = node_get_content (node); if (node_has_name (node, "resourcetype")) { file_type = parse_resourcetype (node); } else if (node_has_name (node, "displayname") && text) { g_file_info_set_display_name (info, text); have_display_name = TRUE; } else if (node_has_name (node, "getetag")) { g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE, text); } else if (node_has_name (node, "creationdate")) { GDateTime *dt; if ((dt = g_date_time_new_from_iso8601 (text, NULL)) == NULL) continue; g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CREATED, g_date_time_to_unix (dt)); g_date_time_unref (dt); } else if (node_has_name (node, "getcontenttype")) { char *ptr; mime_type = g_strdup (text); /* Ignore parameters of the content type */ ptr = strchr (mime_type, ';'); if (ptr) { do *ptr-- = '\0'; while (ptr >= mime_type && g_ascii_isspace (*ptr)); } } else if (node_has_name (node, "getcontentlength")) { gint64 size; size = g_ascii_strtoll (text, NULL, 10); g_file_info_set_size (info, size); } else if (node_has_name (node, "getlastmodified")) { GDateTime *gd; gd = soup_date_time_new_from_http_string (text); if (gd) { g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED, g_date_time_to_unix (gd)); g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, 0); g_date_time_unref (gd); } } } } g_file_info_set_file_type (info, file_type); if (file_type == G_FILE_TYPE_DIRECTORY) { g_clear_pointer (&mime_type, g_free); mime_type = g_strdup ("inode/directory"); icon = g_content_type_get_icon (mime_type); symbolic_icon = g_content_type_get_symbolic_icon (mime_type); file_info_set_content_type (info, mime_type, FALSE); /* Ignore file size for directories. Most of the servers don't report it * for directories anyway. However, some servers report total size of * files inside the directory, which is not expected and causes issues * for clients. */ g_file_info_remove_attribute (info, G_FILE_ATTRIBUTE_STANDARD_SIZE); } else { if (mime_type == NULL) mime_type = g_content_type_guess (basename, NULL, 0, &uncertain_content_type); icon = g_content_type_get_icon (mime_type); if (G_IS_THEMED_ICON (icon)) { g_themed_icon_append_name (G_THEMED_ICON (icon), "text-x-generic"); } symbolic_icon = g_content_type_get_symbolic_icon (mime_type); if (G_IS_THEMED_ICON (icon)) { g_themed_icon_append_name (G_THEMED_ICON (symbolic_icon), "text-x-generic-symbolic"); } file_info_set_content_type (info, mime_type, uncertain_content_type); } if (have_display_name == FALSE) g_file_info_set_display_name (info, basename); g_file_info_set_icon (info, icon); g_file_info_set_symbolic_icon (info, symbolic_icon); g_object_unref (icon); g_object_unref (symbolic_icon); g_free (mime_type); g_free (basename); } static void ms_response_to_fs_info (MsResponse *response, GFileInfo *info) { xmlNodeIter iter; MsPropstat propstat; xmlNodePtr node; guint status; const char *text; guint64 bytes_avail; guint64 bytes_used; gboolean have_bytes_avail; gboolean have_bytes_used; bytes_avail = bytes_used = 0; have_bytes_avail = have_bytes_used = FALSE; ms_response_get_propstat_iter (response, &iter); while (xml_node_iter_next (&iter)) { status = ms_response_get_propstat (&iter, &propstat); if (! SOUP_STATUS_IS_SUCCESSFUL (status)) continue; for (node = propstat.prop_node->children; node; node = node->next) { if (! node_is_element (node)) continue; text = node_get_content (node); if (text == NULL) continue; if (node_has_name (node, "quota-available-bytes")) { if (! string_to_uint64 (text, &bytes_avail)) continue; have_bytes_avail = TRUE; } else if (node_has_name (node, "quota-used-bytes")) { if (! string_to_uint64 (text, &bytes_used)) continue; have_bytes_used = TRUE; } } } if (have_bytes_used) g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_USED, bytes_used); if (have_bytes_avail) { g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, bytes_avail); if (have_bytes_used && G_MAXUINT64 - bytes_avail >= bytes_used) g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE, bytes_avail + bytes_used); } } #define PROPSTAT_XML_BEGIN \ "\n" \ " \n" #define PROPSTAT_XML_ALLPROP " \n" #define PROPSTAT_XML_PROP_BEGIN " \n" #define PROPSTAT_XML_PROP_END " \n" #define PROPSTAT_XML_END \ " " typedef struct _PropName { const char *name; const char *namespace; } PropName; static void propfind_stat_msg_starting_restarted (SoupMessage *msg, gpointer user_data) { soup_message_set_request_body_from_bytes (msg, "application/xml", user_data); } static void propfind_stat_bytes_unref (gpointer data, GClosure *closure) { (void)closure; g_bytes_unref (data); } static SoupMessage * propfind_request_new (GVfsBackend *backend, const char *filename, guint depth, const PropName *properties) { SoupMessage *msg; GUri *uri; const char *header_depth; GString *body; GBytes *bytes; uri = g_vfs_backend_dav_uri_for_path (backend, filename, depth > 0); msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri); g_uri_unref (uri); if (msg == NULL) return NULL; if (depth == 0) header_depth = "0"; else if (depth == 1) header_depth = "1"; else header_depth = "infinity"; soup_message_headers_append (soup_message_get_request_headers (msg), "Depth", header_depth); body = g_string_new (PROPSTAT_XML_BEGIN); if (properties != NULL) { const PropName *prop; g_string_append (body, PROPSTAT_XML_PROP_BEGIN); for (prop = properties; prop->name; prop++) { if (prop->namespace != NULL) g_string_append_printf (body, "<%s xmlns=\"%s\"/>\n", prop->name, prop->namespace); else g_string_append_printf (body, "\n", prop->name); } g_string_append (body, PROPSTAT_XML_PROP_END); } else g_string_append (body, PROPSTAT_XML_ALLPROP); g_string_append (body, PROPSTAT_XML_END); bytes = g_string_free_to_bytes (body); g_signal_connect_data (msg, "starting", G_CALLBACK (propfind_stat_msg_starting_restarted), g_bytes_ref (bytes), propfind_stat_bytes_unref, 0); /* only called with implicit redirects enabled */ g_signal_connect_data (msg, "restarted", G_CALLBACK (propfind_stat_msg_starting_restarted), g_bytes_ref (bytes), propfind_stat_bytes_unref, 0); g_bytes_unref (bytes); return msg; } static SoupMessage * stat_location_start (GUri *uri, gboolean count_children) { SoupMessage *msg; const char *depth; GBytes *bytes; static const char *stat_profind_body = PROPSTAT_XML_BEGIN PROPSTAT_XML_PROP_BEGIN "\n" "\n" PROPSTAT_XML_PROP_END PROPSTAT_XML_END; msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri); if (count_children) depth = "1"; else depth = "0"; soup_message_headers_append (soup_message_get_request_headers (msg), "Depth", depth); bytes = g_bytes_new (stat_profind_body, strlen (stat_profind_body)); g_signal_connect_data (msg, "starting", G_CALLBACK (propfind_stat_msg_starting_restarted), g_bytes_ref (bytes), propfind_stat_bytes_unref, 0); /* only called with implicit redirects enabled */ g_signal_connect_data (msg, "restarted", G_CALLBACK (propfind_stat_msg_starting_restarted), g_bytes_ref (bytes), propfind_stat_bytes_unref, 0); g_bytes_unref (bytes); return msg; } static gboolean stat_location_end (SoupMessage *msg, GInputStream *body, GFileType *target_type, gint64 *target_size, guint *num_children) { Multistatus ms; xmlNodeIter iter; gboolean res; guint child_count; GFileInfo *file_info; if (!body || soup_message_get_status (msg) != SOUP_STATUS_MULTI_STATUS) return FALSE; res = multistatus_parse (msg, body, &ms, NULL); if (res == FALSE) return FALSE; res = FALSE; child_count = 0; file_info = g_file_info_new (); multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; if (! multistatus_get_response (&iter, &response)) continue; if (response.is_target) { ms_response_to_file_info (&response, file_info); res = TRUE; } else child_count++; ms_response_clear (&response); } if (res) { if (target_type) *target_type = g_file_info_get_file_type (file_info); if (target_size) *target_size = g_file_info_get_size (file_info); if (num_children) *num_children = child_count; } multistatus_free (&ms); g_object_unref (file_info); return res; } typedef struct _StatLocationData { GUri *uri; GFileType target_type; gint64 target_size; guint num_children; } StatLocationData; static void stat_location_data_free (gpointer p) { g_uri_unref (((StatLocationData *)p)->uri); g_slice_free (StatLocationData, p); } static void stat_location_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GTask *task = user_data; StatLocationData *data = g_task_get_task_data (task); GInputStream *body; GError *error = NULL; guint status; gboolean res; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { g_object_unref (msg); g_task_return_error (task, error); g_object_unref (task); return; } status = soup_message_get_status (msg); if (status != SOUP_STATUS_MULTI_STATUS) { error = g_error_new (G_IO_ERROR, http_error_code_from_status (status), _("HTTP Error: %s"), soup_message_get_reason_phrase (msg)); g_object_unref (msg); g_task_return_error (task, error); g_object_unref (body); g_object_unref (task); return; } res = stat_location_end (msg, body, &data->target_type, &data->target_size, &data->num_children); g_object_unref (msg); g_object_unref (body); if (res == FALSE) { error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, _("Response invalid")); g_task_return_error (task, error); } else g_task_return_boolean (task, TRUE); g_object_unref (task); } static void stat_location_async (GVfsBackend *backend, GUri *uri, gboolean count_children, GAsyncReadyCallback callback, gpointer user_data) { GTask *task; SoupMessage *msg; StatLocationData *data; task = g_task_new (backend, NULL, callback, user_data); data = g_slice_new (StatLocationData); data->uri = g_uri_ref (uri); g_task_set_source_tag (task, stat_location_async); g_task_set_task_data (task, data, stat_location_data_free); msg = stat_location_start (uri, count_children); if (msg == NULL) { g_task_return_boolean (task, FALSE); g_object_unref (task); return; } dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, stat_location_cb, task); } static gboolean stat_location_finish (GVfsBackend *backend, GFileType *target_type, gint64 *target_size, guint *num_children, GAsyncResult *result, GError **error) { g_return_val_if_fail (G_VFS_IS_BACKEND (backend), FALSE); g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); g_return_val_if_fail (g_task_is_valid (result, backend), FALSE); g_return_val_if_fail (g_async_result_is_tagged (result, stat_location_async), FALSE); if (g_task_propagate_boolean (G_TASK (result), error)) { StatLocationData *data = g_task_get_task_data (G_TASK (result)); if (target_type) *target_type = data->target_type; if (target_size) *target_size = data->target_size; if (num_children) *num_children = data->num_children; return TRUE; } return FALSE; } static GUri * stat_location_async_get_uri (GVfsBackend *backend, GAsyncResult *result) { StatLocationData *data; g_return_val_if_fail (G_VFS_IS_BACKEND (backend), NULL); g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL); g_return_val_if_fail (g_task_is_valid (result, backend), NULL); data = g_task_get_task_data (G_TASK (result)); return data->uri; } /* ************************************************************************* */ /* Authentication */ static void mount_auth_info_free (MountAuthData *data) { if (data->mount_source) g_object_unref (data->mount_source); g_free (data->server_auth.username); g_free (data->server_auth.password); g_free (data->server_auth.realm); g_free (data->proxy_auth.username); g_free (data->proxy_auth.password); } static gboolean soup_authenticate (SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data) { MountAuthData *data; AuthInfo *info; GAskPasswordFlags pw_ask_flags; GPasswordSave pw_save; const char *realm; gboolean res; gboolean aborted; gboolean is_proxy; gboolean have_auth; char *new_username; char *new_password; char *prompt; data = (MountAuthData *) user_data; is_proxy = soup_auth_is_for_proxy (auth); if (is_proxy) info = &(data->proxy_auth); else info = &(data->server_auth); if (!data->interactive) { g_debug ("+ soup_authenticate (%s) \n", retrying ? "retrying" : "first auth"); if (!retrying) soup_auth_authenticate (auth, info->username, info->password); return FALSE; } if (data->retrying_after_403) retrying = TRUE; g_debug ("+ soup_authenticate (interactive, %s) \n", retrying ? "retrying" : "first auth"); new_username = NULL; new_password = NULL; realm = NULL; pw_ask_flags = G_ASK_PASSWORD_NEED_PASSWORD; realm = soup_auth_get_realm (auth); if (realm && info->realm == NULL) info->realm = g_strdup (realm); else if (realm && info->realm && !g_str_equal (realm, info->realm)) return FALSE; have_auth = info->username && info->password; if (have_auth == FALSE && g_vfs_keyring_is_available ()) { const char *host; gchar *hostp = NULL; gint port; pw_ask_flags |= G_ASK_PASSWORD_SAVING_SUPPORTED; if (is_proxy) { gboolean ret; gchar *urip; urip = g_strdup_printf ("http://%s", soup_auth_get_authority (auth)); ret = g_uri_split_network (urip, G_URI_FLAGS_NONE, NULL, &hostp, &port, NULL); g_free (urip); if (!ret) return FALSE; host = hostp; } else { GUri *uri = soup_message_get_uri (msg); host = g_uri_get_host (uri); port = g_uri_get_port (uri); } res = g_vfs_keyring_lookup_password (info->username, host, NULL, "http", realm, is_proxy ? "proxy" : "basic", port, &new_username, NULL, &new_password); if (res == TRUE) { have_auth = TRUE; g_free (info->username); g_free (info->password); info->username = new_username; info->password = new_password; } if (hostp) g_free (hostp); } if (retrying == FALSE && have_auth) { soup_auth_authenticate (auth, info->username, info->password); return FALSE; } if (is_proxy == FALSE) { if (realm == NULL) realm = _("WebDAV share"); /* Translators: %s is the name of the WebDAV share */ prompt = g_strdup_printf (_("Authentication Required\nEnter password for “%s”:"), realm); } else prompt = g_strdup (_("Authentication Required\nEnter proxy password:")); if (info->username == NULL) pw_ask_flags |= G_ASK_PASSWORD_NEED_USERNAME; res = g_mount_source_ask_password (data->mount_source, prompt, info->username, NULL, pw_ask_flags, &aborted, &new_password, &new_username, NULL, NULL, &pw_save); if (res && !aborted) { /* it's not safe to assume that we get the username filed in, in the case that we provied a default username */ if (new_username == NULL) new_username = g_strdup (info->username); soup_auth_authenticate (auth, new_username, new_password); g_free (info->username); g_free (info->password); info->username = new_username; info->password = new_password; info->pw_save = pw_save; } else soup_auth_cancel (auth); g_debug ("- soup_authenticate \n"); g_free (prompt); return !res || aborted; } static void keyring_save_authinfo (AuthInfo *info, GUri *uri, gboolean is_proxy) { const char *type = is_proxy ? "proxy" : "basic"; g_vfs_keyring_save_password (info->username, g_uri_get_host (uri), NULL, "http", info->realm, type, g_uri_get_port (uri), info->password, info->pw_save); } /* ************************************************************************* */ static GUri * g_mount_spec_to_dav_uri (GMountSpec *spec) { GUri *uri; const char *host; const char *port; const char *ssl; const char *path; const char *scheme; char *host_str; char *path_str; gint port_num; host = g_mount_spec_get (spec, "host"); port = g_mount_spec_get (spec, "port"); ssl = g_mount_spec_get (spec, "ssl"); path = spec->mount_prefix; if (host == NULL || *host == 0) return NULL; if (ssl != NULL && (strcmp (ssl, "true") == 0)) scheme = "https"; else scheme = "http"; port_num = -1; /* always a valid number if set */ if (port != NULL) port_num = atoi (port); /* IPv6 host does not include brackets in SoupURI, but GMountSpec host does */ if (gvfs_is_ipv6 (host)) host_str = g_strndup (host + 1, strlen (host) - 2); else host_str = g_strdup (host); path_str = dav_uri_encode (path); /* A username is not part of URI as a workaround for: * https://gitlab.gnome.org/GNOME/gvfs/-/issues/617. */ uri = g_uri_build (SOUP_HTTP_URI_FLAGS, scheme, NULL, host_str, port_num, path_str, NULL, NULL); g_free (host_str); g_free (path_str); return uri; } static GMountSpec * g_mount_spec_from_dav_uri (GVfsBackendDav *dav_backend, GUri *uri) { GMountSpec *spec; char *local_path; gboolean ssl; gint port_num; #ifdef HAVE_AVAHI if (dav_backend->resolver != NULL) { const char *type; const char *service_type; service_type = g_vfs_dns_sd_resolver_get_service_type (dav_backend->resolver); if (strcmp (service_type, "_webdavs._tcp") == 0) type = "davs+sd"; else type = "dav+sd"; spec = g_mount_spec_new (type); g_mount_spec_set (spec, "host", g_vfs_dns_sd_resolver_get_encoded_triple (dav_backend->resolver)); return spec; } #endif spec = g_mount_spec_new ("dav"); /* IPv6 host does not include brackets in GUri, but GMountSpec host does */ if (strchr (g_uri_get_host (uri), ':')) { char *host = g_strdup_printf ("[%s]", g_uri_get_host (uri)); g_mount_spec_set (spec, "host", host); g_free (host); } else g_mount_spec_set (spec, "host", g_uri_get_host (uri)); ssl = !strcmp (g_uri_get_scheme (uri), "https"); g_mount_spec_set (spec, "ssl", ssl ? "true" : "false"); port_num = g_uri_get_port (uri); if (port_num > 0 && port_num != (ssl ? 443 : 80)) { char *port = g_strdup_printf ("%d", port_num); g_mount_spec_set (spec, "port", port); g_free (port); } /* There must not be any illegal characters in the URL at this point */ local_path = g_uri_unescape_string (g_uri_get_path (uri), "/"); g_mount_spec_set_mount_prefix (spec, local_path); g_free (local_path); return spec; } #ifdef HAVE_AVAHI static GUri * dav_uri_from_dns_sd_resolver (GVfsBackendDav *dav_backend) { GUri *uri; char *user; char *path; char *address; char *host; gchar *interface; const char *service_type; const char *scheme; guint port; service_type = g_vfs_dns_sd_resolver_get_service_type (dav_backend->resolver); address = g_vfs_dns_sd_resolver_get_address (dav_backend->resolver); interface = g_vfs_dns_sd_resolver_get_interface (dav_backend->resolver); port = g_vfs_dns_sd_resolver_get_port (dav_backend->resolver); user = g_vfs_dns_sd_resolver_lookup_txt_record (dav_backend->resolver, "u"); /* mandatory */ path = g_vfs_dns_sd_resolver_lookup_txt_record (dav_backend->resolver, "path"); /* optional */ /* TODO: According to http://www.dns-sd.org/ServiceTypes.html * there's also a TXT record "p" for password. Handle this. */ if (strcmp (service_type, "_webdavs._tcp") == 0) scheme = "https"; else scheme = "http"; /* IPv6 host does not include brackets in GUri, but GVfsDnsSdResolver host does */ if (gvfs_is_ipv6 (address)) { /* Link-local addresses require interface to be specified. */ if (g_str_has_prefix (address, "[fe80:") && interface != NULL) { host = g_strconcat (address + 1, interface, NULL); host[strlen (address) - 2] = '%'; } else host = g_strndup (address + 1, strlen (address) - 2); } else host = g_strdup (address); uri = g_uri_build_with_user (SOUP_HTTP_URI_FLAGS, scheme, user, NULL, NULL, host, port, path, NULL, NULL); g_free (address); g_free (interface); g_free (user); g_free (path); g_free (host); return uri; } #endif #ifdef HAVE_AVAHI static void dns_sd_resolver_changed (GVfsDnsSdResolver *resolver, GVfsBackendDav *dav_backend) { /* If anything has changed (e.g. address, port, txt-records or is-resolved), * it is safest to just unmount. */ g_vfs_backend_force_unmount (G_VFS_BACKEND (dav_backend)); } #endif /* ************************************************************************* */ /* Backend Functions */ static void mount_success (GVfsBackend *backend, GVfsJobMount *job) { GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend); GMountSpec *mount_spec; GUri *tmp; const gchar *user; /* Save the auth info in the keyring */ keyring_save_authinfo (&(dav_backend->auth_info.server_auth), http_backend->mount_base, FALSE); /* TODO: save proxy auth */ /* Set the working path in mount path */ tmp = http_backend->mount_base; http_backend->mount_base = soup_uri_copy (tmp, SOUP_URI_PATH, dav_backend->last_good_path, SOUP_URI_NONE); g_uri_unref (tmp); g_clear_pointer (&dav_backend->last_good_path, g_free); /* dup the mountspec, but only copy known fields */ mount_spec = g_mount_spec_from_dav_uri (dav_backend, http_backend->mount_base); /* A username is not part of URI as a workaround for: * https://gitlab.gnome.org/GNOME/gvfs/-/issues/617 * So it has to be restored here in order to avoid: * https://gitlab.gnome.org/GNOME/gvfs/-/issues/614 */ user = g_mount_spec_get (job->mount_spec, "user"); if (user != NULL) g_mount_spec_set (mount_spec, "user", user); g_vfs_backend_set_mount_spec (backend, mount_spec); g_vfs_backend_set_icon_name (backend, "folder-remote"); g_vfs_backend_set_symbolic_icon_name (backend, "folder-remote-symbolic"); g_vfs_backend_dav_setup_display_name (backend); /* cleanup */ g_mount_spec_unref (mount_spec); g_vfs_job_succeeded (G_VFS_JOB (job)); g_debug ("- mount\n"); } static void try_mount_opts_cb (GObject *source, GAsyncResult *result, gpointer user_data); static void try_mount_stat_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend); GVfsJobMount *job = user_data; SoupMessage *msg_stat = g_vfs_backend_dav_get_async_result_message (backend, result); SoupMessage *msg_opts; GInputStream *body; GError *error = NULL; GFileType file_type; gboolean res; gboolean is_collection; GUri *tmp; char *new_path; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (body == NULL) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); goto clear_msg; } res = stat_location_end (msg_stat, body, &file_type, NULL, NULL); is_collection = res && file_type == G_FILE_TYPE_DIRECTORY; if (!g_vfs_backend_dav_stream_skip (body, &error)) { g_object_unref (body); if (dav_backend->last_good_path == NULL) g_vfs_job_failed_from_error (G_VFS_JOB (job), error); else mount_success (backend, job); g_error_free (error); goto clear_msg; } g_object_unref (body); soup_message_headers_clear (soup_message_get_response_headers (msg_stat)); g_debug (" [%s] webdav: %d, collection %d [res: %d]\n", g_uri_get_path (http_backend->mount_base), TRUE, is_collection, res); if ((is_collection == FALSE) && (dav_backend->last_good_path != NULL)) { mount_success (backend, job); goto clear_msg; } else if (res == FALSE) { int error_code = http_error_code_from_status (soup_message_get_status (msg_stat)); g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, error_code, _("HTTP Error: %s"), soup_message_get_reason_phrase (msg_stat)); goto clear_msg; } else if (is_collection == FALSE) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Could not find an enclosing directory")); goto clear_msg; } /* we have found a new good root, try the parent ... */ g_free (dav_backend->last_good_path); dav_backend->last_good_path = g_strdup (g_uri_get_path (http_backend->mount_base)); new_path = path_get_parent_dir (dav_backend->last_good_path); tmp = http_backend->mount_base; http_backend->mount_base = soup_uri_copy (tmp, SOUP_URI_PATH, new_path, SOUP_URI_NONE); g_uri_unref (tmp); g_free (new_path); /* if we have found a root that is good then we assume that we also have obtained to correct credentials and we switch the auth handler. This will prevent us from asking for *different* credentials *again* if the server should response with 401 for some of the parent collections. See also bug #677753 */ dav_backend->auth_info.interactive = FALSE; /* break out */ if (g_strcmp0 (dav_backend->last_good_path, "/") == 0) { mount_success (backend, job); goto clear_msg; } /* else loop the whole thing from options */ msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, http_backend->mount_base); dav_message_connect_signals (msg_opts, backend); g_vfs_backend_dav_send_async (backend, msg_opts, try_mount_opts_cb, job); clear_msg: g_object_unref (msg_stat); } static void try_mount_opts_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend); GVfsJobMount *job = user_data; SoupMessage *msg_opts = g_vfs_backend_dav_get_async_result_message (backend, result); SoupMessage *msg_stat; GInputStream *body; GError *error = NULL; GUri *cur_uri; guint status; gboolean is_success, is_webdav; body = g_vfs_backend_dav_send_finish (backend, result, &error); /* If SSL is used and the certificate verifies OK, then ssl-strict remains * on for all further connections. * If SSL is used and the certificate does not verify OK, then the user * gets a chance to override it. If they do, ssl-strict is disabled but * the certificate is stored, and checked on each subsequent connection to * ensure that it hasn't changed. */ if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE) && !dav_backend->certificate_errors) { GTlsCertificate *certificate; GTlsCertificateFlags errors; certificate = soup_message_get_tls_peer_certificate (msg_opts); errors = soup_message_get_tls_peer_certificate_errors (msg_opts); if (gvfs_accept_certificate (dav_backend->auth_info.mount_source, certificate, errors)) { g_clear_error (&error); dav_backend->certificate = g_object_ref (certificate); dav_backend->certificate_errors = errors; /* re-send the opts message; re-create since we're still in its cb */ g_object_unref (msg_opts); msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, http_backend->mount_base); dav_message_connect_signals (msg_opts, backend); g_vfs_backend_dav_send_async (backend, msg_opts, try_mount_opts_cb, job); return; } } /* message failed, propagate the error */ if (!body) { if (dav_backend->last_good_path == NULL) g_vfs_job_failed_from_error (G_VFS_JOB (job), error); else mount_success (backend, job); g_error_free (error); goto clear_msgs; } status = soup_message_get_status (msg_opts); /* Workaround for servers which response with 403 instead of 401 in case of * wrong credentials to let the user specify its credentials again. */ if (status == SOUP_STATUS_FORBIDDEN && dav_backend->last_good_path == NULL && (dav_backend->auth_info.server_auth.password != NULL || dav_backend->auth_info.proxy_auth.password != NULL)) { SoupSessionFeature *auth_manager; dav_backend->auth_info.retrying_after_403 = TRUE; g_clear_pointer (&dav_backend->auth_info.server_auth.username, g_free); dav_backend->auth_info.server_auth.username = g_strdup (g_uri_get_user (http_backend->mount_base)); g_clear_pointer (&dav_backend->auth_info.server_auth.password, g_free); g_clear_pointer (&dav_backend->auth_info.proxy_auth.password, g_free); auth_manager = soup_session_get_feature (http_backend->session, SOUP_TYPE_AUTH_MANAGER); soup_auth_manager_clear_cached_credentials (SOUP_AUTH_MANAGER (auth_manager)); /* re-send the opts message; re-create since we're still in its cb */ g_object_unref (msg_opts); msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, http_backend->mount_base); dav_message_connect_signals (msg_opts, backend); g_vfs_backend_dav_send_async (backend, msg_opts, try_mount_opts_cb, job); return; } is_success = SOUP_STATUS_IS_SUCCESSFUL (status); is_webdav = sm_has_header (msg_opts, "DAV"); if ((is_success && !is_webdav) || (status == SOUP_STATUS_METHOD_NOT_ALLOWED)) { if (dav_backend->last_good_path == NULL) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Not a WebDAV enabled share")); else mount_success (backend, job); goto clear_msgs; } else if (!is_success) { int error_code = http_error_code_from_status (soup_message_get_status (msg_opts)); if (dav_backend->last_good_path == NULL) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, error_code, _("HTTP Error: %s"), soup_message_get_reason_phrase (msg_opts)); else mount_success (backend, job); goto clear_msgs; } if (!g_vfs_backend_dav_stream_skip (body, &error)) { if (dav_backend->last_good_path == NULL) g_vfs_job_failed_from_error (G_VFS_JOB (job), error); else mount_success (backend, job); g_error_free (error); goto clear_msgs; } g_object_unref (body); cur_uri = soup_message_get_uri (msg_opts); /* The count_children parameter is intentionally set to TRUE to be sure that enumeration is possible: https://gitlab.gnome.org/GNOME/gvfs/-/issues/468 */ msg_stat = stat_location_start (cur_uri, TRUE); dav_message_connect_signals (msg_stat, backend); g_vfs_backend_dav_send_async (backend, msg_stat, try_mount_stat_cb, job); clear_msgs: g_object_unref (msg_opts); } static void try_mount_send_opts (GVfsJobMount *job) { GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (job->backend); GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (job->backend); SoupMessage *msg_opts; const gchar *user; if (http_backend->mount_base == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Invalid mount spec")); return; } soup_session_add_feature_by_type (http_backend->session, SOUP_TYPE_AUTH_NEGOTIATE); soup_session_add_feature_by_type (http_backend->session, SOUP_TYPE_AUTH_NTLM); user = g_mount_spec_get (job->mount_spec, "user"); dav_backend->auth_info.mount_source = g_object_ref (job->mount_source); dav_backend->auth_info.server_auth.username = g_strdup (user); dav_backend->auth_info.server_auth.password = NULL; dav_backend->auth_info.server_auth.pw_save = G_PASSWORD_SAVE_NEVER; dav_backend->auth_info.proxy_auth.pw_save = G_PASSWORD_SAVE_NEVER; dav_backend->auth_info.interactive = TRUE; dav_backend->last_good_path = NULL; msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, http_backend->mount_base); dav_message_connect_signals (msg_opts, job->backend); g_vfs_backend_dav_send_async (job->backend, msg_opts, try_mount_opts_cb, job); } #ifdef HAVE_AVAHI static void try_mount_resolve_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsJobMount *job = user_data; GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (job->backend); GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (job->backend); GError *error = NULL; if (!g_vfs_dns_sd_resolver_resolve_finish (dav_backend->resolver, result, &error)) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); return; } g_signal_connect (dav_backend->resolver, "changed", (GCallback) dns_sd_resolver_changed, dav_backend); http_backend->mount_base = dav_uri_from_dns_sd_resolver (dav_backend); try_mount_send_opts (job); } #endif static gboolean try_mount (GVfsBackend *backend, GVfsJobMount *job, GMountSpec *mount_spec, GMountSource *mount_source, gboolean is_automount) { GVfsBackendHttp *http_backend = G_VFS_BACKEND_HTTP (backend); g_debug ("+ mount\n"); #ifdef HAVE_AVAHI GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); const char *host; const char *type; host = g_mount_spec_get (mount_spec, "host"); type = g_mount_spec_get (mount_spec, "type"); /* resolve DNS-SD style URIs */ if ((strcmp (type, "dav+sd") == 0 || strcmp (type, "davs+sd") == 0) && host != NULL) { dav_backend->resolver = g_vfs_dns_sd_resolver_new_for_encoded_triple (host, "u"); g_vfs_dns_sd_resolver_resolve (dav_backend->resolver, G_VFS_JOB (job)->cancellable, try_mount_resolve_cb, job); } else #endif { http_backend->mount_base = g_mount_spec_to_dav_uri (mount_spec); try_mount_send_opts (job); } return TRUE; } static PropName ls_propnames[] = { {"creationdate", NULL}, {"displayname", NULL}, {"getcontentlength", NULL}, {"getcontenttype", NULL}, {"getetag", NULL}, {"getlastmodified", NULL}, {"resourcetype", NULL}, {NULL, NULL} }; /* *** query_info () *** */ static void try_query_info_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GVfsJobQueryInfo *job = user_data; GInputStream *body; GError *error = NULL; Multistatus ms; xmlNodeIter iter; gboolean res; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) goto error; res = multistatus_parse (msg, body, &ms, &error); g_object_unref (body); if (res == FALSE) goto error; res = FALSE; multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; if (! multistatus_get_response (&iter, &response)) continue; if (response.is_target) { ms_response_to_file_info (&response, job->file_info); res = TRUE; } ms_response_clear (&response); } multistatus_free (&ms); g_object_unref (msg); if (res) g_vfs_job_succeeded (G_VFS_JOB (job)); else g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Response invalid")); return; error: g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); } static gboolean try_query_info (GVfsBackend *backend, GVfsJobQueryInfo *job, const char *filename, GFileQueryInfoFlags flags, GFileInfo *info, GFileAttributeMatcher *matcher) { SoupMessage *msg; g_debug ("Query info %s\n", filename); msg = propfind_request_new (backend, filename, 0, ls_propnames); if (msg == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Could not create request")); return TRUE; } message_add_redirect_header (msg, flags); dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, try_query_info_cb, job); return TRUE; } static PropName fs_info_propnames[] = { {"quota-available-bytes", NULL}, {"quota-used-bytes", NULL}, {NULL, NULL} }; static void try_query_fs_info_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GVfsJobQueryFsInfo *job = user_data; GInputStream *body; GError *error = NULL; Multistatus ms; xmlNodeIter iter; gboolean res; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) goto error; res = multistatus_parse (msg, body, &ms, &error); g_object_unref (body); if (res == FALSE) goto error; res = FALSE; multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; if (! multistatus_get_response (&iter, &response)) continue; if (response.is_target) { ms_response_to_fs_info (&response, job->file_info); res = TRUE; } ms_response_clear (&response); } multistatus_free (&ms); g_object_unref (msg); if (res) g_vfs_job_succeeded (G_VFS_JOB (job)); else g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Response invalid")); return; error: g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); } static gboolean try_query_fs_info (GVfsBackend *backend, GVfsJobQueryFsInfo *job, const char *filename, GFileInfo *info, GFileAttributeMatcher *attribute_matcher) { SoupMessage *msg; g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, "webdav"); g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_FILESYSTEM_REMOTE, TRUE); g_file_info_set_attribute_uint32 (info, G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW, G_FILESYSTEM_PREVIEW_TYPE_IF_ALWAYS); if (! (g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE) || g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_USED) || g_file_attribute_matcher_matches (attribute_matcher, G_FILE_ATTRIBUTE_FILESYSTEM_FREE))) { g_vfs_job_succeeded (G_VFS_JOB (job)); return TRUE; } msg = propfind_request_new (backend, filename, 0, fs_info_propnames); if (msg == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Could not create request")); return TRUE; } dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, try_query_fs_info_cb, job); return TRUE; } /* *** enumerate *** */ static void try_enumerate_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GVfsJobEnumerate *job = user_data; GInputStream *body; GError *error = NULL; Multistatus ms; xmlNodeIter iter; gboolean res; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) goto error; res = multistatus_parse (msg, body, &ms, &error); g_object_unref (body); if (res == FALSE) goto error; g_vfs_job_succeeded (G_VFS_JOB (job)); multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; GFileInfo *info; if (! multistatus_get_response (&iter, &response)) continue; if (response.is_target == FALSE) { info = g_file_info_new (); ms_response_to_file_info (&response, info); g_vfs_job_enumerate_add_info (job, info); g_object_unref (info); } ms_response_clear (&response); } multistatus_free (&ms); g_object_unref (msg); g_vfs_job_enumerate_done (G_VFS_JOB_ENUMERATE (job)); return; error: g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); } static gboolean try_enumerate (GVfsBackend *backend, GVfsJobEnumerate *job, const char *filename, GFileAttributeMatcher *matcher, GFileQueryInfoFlags flags) { SoupMessage *msg; g_debug ("+ try_enumerate: %s\n", filename); msg = propfind_request_new (backend, filename, 1, ls_propnames); if (msg == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Could not create request")); return TRUE; } message_add_redirect_header (msg, flags); dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, try_enumerate_cb, job); return TRUE; } /* ************************************************************************* */ /* */ /* *** open () *** */ static void try_open_stat_done (GObject *source, GAsyncResult *result, gpointer user_data) { GInputStream *body; SoupSession *session = SOUP_SESSION (source); SoupMessage *msg; GVfsJob *job = G_VFS_JOB (user_data); GVfsBackend *backend = job->backend_data; GError *error = NULL; GUri *uri; GFileType target_type; gboolean res; msg = soup_session_get_async_result_message (session, result); if (soup_message_get_status (msg) != SOUP_STATUS_MULTI_STATUS) { http_job_failed (job, msg); return; } body = soup_session_send_finish (session, result, &error); if (!body) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return; } res = stat_location_end (msg, body, &target_type, NULL, NULL); g_object_unref (body); if (res == FALSE) { g_vfs_job_failed (job, G_IO_ERROR, G_IO_ERROR_FAILED, _("Response invalid")); return; } if (target_type == G_FILE_TYPE_DIRECTORY) { g_vfs_job_failed (job, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("File is directory")); return; } uri = soup_message_get_uri (msg); http_backend_open_for_read (backend, job, uri); } static gboolean try_open_for_read (GVfsBackend *backend, GVfsJobOpenForRead *job, const char *filename) { SoupMessage *msg; GUri *uri; uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE); msg = stat_location_start (uri, FALSE); g_uri_unref (uri); if (msg == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Could not create request")); return FALSE; } dav_message_connect_signals (msg, backend); g_vfs_job_set_backend_data (G_VFS_JOB (job), backend, NULL); soup_session_send_async (G_VFS_BACKEND_HTTP (backend)->session, msg, G_PRIORITY_DEFAULT, NULL, try_open_stat_done, job); return TRUE; } /* *** create () *** */ static void try_create_tested_existence (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsJob *job = G_VFS_JOB (user_data); GOutputStream *stream; GInputStream *body; SoupMessage *msg = job->backend_data; SoupMessage *put_msg; GError *error = NULL; GUri *uri; body = soup_session_send_finish (SOUP_SESSION (source), result, &error); if (!body) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return; } if (SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) { g_object_unref (body); g_vfs_job_failed (job, G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); return; } /* TODO: other errors */ g_object_unref (body); uri = soup_message_get_uri (msg); put_msg = soup_message_new_from_uri (SOUP_METHOD_PUT, uri); /* * Doesn't work with apache > 2.2.9 * soup_message_headers_append (soup_message_get_request_headers (put_msg), "If-None-Match", "*"); */ stream = g_memory_output_stream_new (NULL, 0, g_try_realloc, g_free); g_object_set_data_full (G_OBJECT (stream), "-gvfs-stream-msg", put_msg, g_object_unref); g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream); g_vfs_job_open_for_write_set_can_seek (G_VFS_JOB_OPEN_FOR_WRITE (job), g_seekable_can_seek (G_SEEKABLE (stream))); g_vfs_job_open_for_write_set_can_truncate (G_VFS_JOB_OPEN_FOR_WRITE (job), g_seekable_can_truncate (G_SEEKABLE (stream))); g_vfs_job_succeeded (job); } static gboolean try_create (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, GFileCreateFlags flags) { SoupMessage *msg; GUri *uri; /* TODO: if we supported chunked requests, we could * use a PUT with "If-None-Match: *" and "Expect: 100-continue" */ uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE); msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri); g_uri_unref (uri); g_vfs_job_set_backend_data (G_VFS_JOB (job), msg, NULL); dav_message_connect_signals (msg, backend); soup_session_send_async (G_VFS_BACKEND_HTTP (backend)->session, msg, G_PRIORITY_DEFAULT, NULL, try_create_tested_existence, job); return TRUE; } /* *** replace () *** */ static void open_for_replace_succeeded (GVfsBackendHttp *op_backend, GVfsJob *job, GUri *uri, const char *etag) { SoupMessage *put_msg; GOutputStream *stream; put_msg = soup_message_new_from_uri (SOUP_METHOD_PUT, uri); if (etag) soup_message_headers_append (soup_message_get_request_headers (put_msg), "If-Match", etag); stream = g_memory_output_stream_new (NULL, 0, g_try_realloc, g_free); g_object_set_data_full (G_OBJECT (stream), "-gvfs-stream-msg", put_msg, g_object_unref); g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream); g_vfs_job_open_for_write_set_can_seek (G_VFS_JOB_OPEN_FOR_WRITE (job), g_seekable_can_seek (G_SEEKABLE (stream))); g_vfs_job_open_for_write_set_can_truncate (G_VFS_JOB_OPEN_FOR_WRITE (job), g_seekable_can_truncate (G_SEEKABLE (stream))); g_vfs_job_succeeded (job); } static void try_replace_checked_etag (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsJob *job = G_VFS_JOB (user_data); GVfsBackendHttp *op_backend = job->backend_data; GInputStream *body; SoupSession *session = SOUP_SESSION (source); SoupMessage *msg; GError *error; msg = soup_session_get_async_result_message (session, result); body = soup_session_send_finish (session, result, &error); if (!body) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return; } if (soup_message_get_status (msg) == SOUP_STATUS_PRECONDITION_FAILED) { g_object_unref (body); g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_WRONG_ETAG, _("The file was externally modified")); return; } /* TODO: other errors */ g_object_unref (body); open_for_replace_succeeded (op_backend, job, soup_message_get_uri (msg), soup_message_headers_get_one (soup_message_get_request_headers (msg), "If-Match")); } static gboolean try_replace (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, const char *etag, gboolean make_backup, GFileCreateFlags flags) { GVfsBackendHttp *op_backend; GUri *uri; /* TODO: if SoupOutputStream supported chunked requests, we could * use a PUT with "If-Match: ..." and "Expect: 100-continue" */ op_backend = G_VFS_BACKEND_HTTP (backend); if (make_backup) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_CANT_CREATE_BACKUP, _("Backup file creation failed")); return TRUE; } uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE); if (etag) { SoupMessage *msg; msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri); g_uri_unref (uri); dav_message_connect_signals (msg, backend); soup_message_headers_append (soup_message_get_request_headers (msg), "If-Match", etag); g_vfs_job_set_backend_data (G_VFS_JOB (job), op_backend, NULL); soup_session_send_async (op_backend->session, msg, G_PRIORITY_DEFAULT, NULL, try_replace_checked_etag, job); return TRUE; } open_for_replace_succeeded (op_backend, G_VFS_JOB (job), uri, NULL); g_uri_unref (uri); return TRUE; } /* *** write () *** */ static void write_ready (GObject *source_object, GAsyncResult *result, gpointer user_data) { GOutputStream *stream; GVfsJob *job; GError *error; gssize nwrote; stream = G_OUTPUT_STREAM (source_object); error = NULL; job = G_VFS_JOB (user_data); nwrote = g_output_stream_write_finish (stream, result, &error); if (nwrote < 0) { g_vfs_job_failed_literal (G_VFS_JOB (job), error->domain, error->code, error->message); g_error_free (error); return; } g_vfs_job_write_set_written_size (G_VFS_JOB_WRITE (job), nwrote); g_vfs_job_succeeded (job); } static gboolean try_write (GVfsBackend *backend, GVfsJobWrite *job, GVfsBackendHandle handle, char *buffer, gsize buffer_size) { GOutputStream *stream; stream = G_OUTPUT_STREAM (handle); g_output_stream_write_async (stream, buffer, buffer_size, G_PRIORITY_DEFAULT, G_VFS_JOB (job)->cancellable, write_ready, job); return TRUE; } /* this does not invoke libsoup API, so it can be synchronous/threaded */ static void do_seek_on_write (GVfsBackend *backend, GVfsJobSeekWrite *job, GVfsBackendHandle handle, goffset offset, GSeekType type) { GSeekable *stream = G_SEEKABLE (handle); GError *error = NULL; if (g_seekable_seek (stream, offset, type, G_VFS_JOB (job)->cancellable, &error)) { g_vfs_job_seek_write_set_offset (job, g_seekable_tell (stream)); g_vfs_job_succeeded (G_VFS_JOB (job)); } else { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); } } /* this does not invoke libsoup API, so it can be synchronous/threaded */ static void do_truncate (GVfsBackend *backend, GVfsJobTruncate *job, GVfsBackendHandle handle, goffset size) { GSeekable *stream = G_SEEKABLE (handle); GError *error = NULL; if (g_seekable_truncate (stream, size, G_VFS_JOB (job)->cancellable, &error)) { g_vfs_job_succeeded (G_VFS_JOB (job)); } else { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); } } /* *** close_write () *** */ static void try_close_write_sent (GObject *source, GAsyncResult *result, gpointer user_data) { GInputStream *body; GVfsJob *job = G_VFS_JOB (user_data); SoupSession *session = SOUP_SESSION (source); SoupMessage *msg = soup_session_get_async_result_message (session, result); GError *error; body = soup_session_send_finish (session, result, &error); if (!body) { g_vfs_job_failed_from_error (job, error); g_error_free (error); return; } g_object_unref (body); if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) http_job_failed (job, msg); else g_vfs_job_succeeded (job); } static gboolean try_close_write (GVfsBackend *backend, GVfsJobCloseWrite *job, GVfsBackendHandle handle) { GOutputStream *stream; SoupMessage *msg; GBytes *bytes; stream = G_OUTPUT_STREAM (handle); msg = g_object_get_data (G_OBJECT (stream), "-gvfs-stream-msg"); g_object_ref (msg); g_object_set_data (G_OBJECT (stream), "-gvfs-stream-msg", NULL); dav_message_connect_signals (msg, backend); g_output_stream_close (stream, NULL, NULL); bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (stream)); g_object_unref (stream); soup_message_set_request_body_from_bytes (msg, NULL, bytes); soup_session_send_async (G_VFS_BACKEND_HTTP (backend)->session, msg, G_PRIORITY_DEFAULT, NULL, try_close_write_sent, job); g_bytes_unref (bytes); return TRUE; } static void make_directory_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); GVfsJobMakeDirectory *job = user_data; SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GInputStream *body; GError *error = NULL; guint status; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); return; } status = soup_message_get_status (msg); if (! SOUP_STATUS_IS_SUCCESSFUL (status)) if (status == SOUP_STATUS_METHOD_NOT_ALLOWED) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); else http_job_failed (G_VFS_JOB (job), msg); else g_vfs_job_succeeded (G_VFS_JOB (job)); g_object_unref (body); g_object_unref (msg); } static gboolean try_make_directory (GVfsBackend *backend, GVfsJobMakeDirectory *job, const char *filename) { SoupMessage *msg; GUri *uri; uri = g_vfs_backend_dav_uri_for_path (backend, filename, TRUE); msg = soup_message_new_from_uri (SOUP_METHOD_MKCOL, uri); g_uri_unref (uri); dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, make_directory_cb, job); return TRUE; } static void try_delete_send_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); GVfsJobDelete *job = user_data; SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GInputStream *body; GError *error = NULL; guint status; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); return; } status = soup_message_get_status (msg); if (!SOUP_STATUS_IS_SUCCESSFUL (status)) http_job_failed (G_VFS_JOB (job), msg); else g_vfs_job_succeeded (G_VFS_JOB (job)); g_object_unref (msg); g_object_unref (body); } static void try_delete_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); GVfsJobDelete *job = user_data; SoupMessage *msg = NULL; GFileType file_type; guint num_children; GError *error = NULL; gboolean res; GUri *uri = stat_location_async_get_uri (backend, result); res = stat_location_finish (backend, &file_type, NULL, &num_children, result, &error); if (res == FALSE) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_clear_error (&error); return; } if (file_type == G_FILE_TYPE_DIRECTORY && num_children) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_EMPTY, _("Directory not empty")); return; } msg = soup_message_new_from_uri (SOUP_METHOD_DELETE, uri); dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, try_delete_send_cb, job); } static gboolean try_delete (GVfsBackend *backend, GVfsJobDelete *job, const char *filename) { GUri *uri; uri = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE); stat_location_async (backend, uri, TRUE, try_delete_cb, job); return TRUE; } typedef struct _TrySetDisplayNameData { GVfsJobSetDisplayName *job; char *target_path; } TrySetDisplayNameData; static void try_set_display_name_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); TrySetDisplayNameData *data = user_data; SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); GInputStream *body; GError *error = NULL; guint status; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { http_job_failed (G_VFS_JOB (data->job), msg); goto error; } status = soup_message_get_status (msg); /* * The precondition of SOUP_STATUS_PRECONDITION_FAILED (412) in * this case was triggered by the "Overwrite: F" header which * means that the target already exists. * Also if we get a REDIRECTION it means that there was no * "Location" header, since otherwise that would have triggered * our redirection handler. This probably means we are dealing * with an web dav implementation (like mod_dav) that also sends * redirects for the destionaion (i.e. "Destination: /foo" header) * which very likely means that the target also exists (and is a * directory). That or the webdav server is broken. * We could find out by doing another stat and but I think this is * such a corner case that we are totally fine with returning * G_IO_ERROR_EXISTS. * */ if (SOUP_STATUS_IS_SUCCESSFUL (status)) { g_debug ("new target_path: %s\n", data->target_path); g_vfs_job_set_display_name_set_new_path (data->job, data->target_path); g_vfs_job_succeeded (G_VFS_JOB (data->job)); } else if (status == SOUP_STATUS_PRECONDITION_FAILED || SOUP_STATUS_IS_REDIRECTION (status)) g_vfs_job_failed (G_VFS_JOB (data->job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); else http_job_failed (G_VFS_JOB (data->job), msg); g_object_unref (body); error: g_clear_error (&error); g_object_unref (msg); g_free (data->target_path); g_slice_free (TrySetDisplayNameData, data); } static gboolean try_set_display_name (GVfsBackend *backend, GVfsJobSetDisplayName *job, const char *filename, const char *display_name) { SoupMessage *msg; GUri *source; GUri *target; TrySetDisplayNameData *data = g_slice_new (TrySetDisplayNameData); char *dirname; source = g_vfs_backend_dav_uri_for_path (backend, filename, FALSE); msg = soup_message_new_from_uri (SOUP_METHOD_MOVE, source); dirname = g_path_get_dirname (filename); data->target_path = g_build_filename (dirname, display_name, NULL); target = g_vfs_backend_dav_uri_for_path (backend, data->target_path, FALSE); message_add_destination_header (msg, target); message_add_overwrite_header (msg, FALSE); data->job = job; g_free (dirname); g_uri_unref (target); g_uri_unref (source); dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, try_set_display_name_cb, data); return TRUE; } typedef struct _CopyData { GVfsJob *job; SoupMessage *msg; GUri *source_uri; GUri *target_uri; GFileProgressCallback progress_callback; gpointer progress_callback_data; gint64 file_size; GFileType source_ft; GFileType target_ft; GFileCopyFlags flags; gboolean source_res; gboolean target_res; } CopyData; static void copy_data_free (gpointer data) { CopyData *p = data; g_clear_pointer (&p->source_uri, g_uri_unref); g_clear_pointer (&p->target_uri, g_uri_unref); g_clear_object (&p->msg); g_slice_free (CopyData, p); } static void try_move_do_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); CopyData *data = user_data; GInputStream *body; GError *error = NULL; guint status; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { g_vfs_job_failed_from_error (data->job, error); copy_data_free (data); g_clear_error (&error); return; } /* See try_set_display_name () for the explanation of the PRECONDITION_FAILED * and IS_REDIRECTION handling below. */ status = soup_message_get_status (data->msg); if (SOUP_STATUS_IS_SUCCESSFUL (status)) { if (data->source_res && data->progress_callback) data->progress_callback (data->file_size, data->file_size, data->progress_callback_data); g_vfs_job_succeeded (data->job); } else if (status == SOUP_STATUS_PRECONDITION_FAILED || SOUP_STATUS_IS_REDIRECTION (status)) g_vfs_job_failed (data->job, G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); else http_job_failed (data->job, data->msg); copy_data_free (data); } static void try_move_do (GVfsBackend *backend, CopyData *data) { message_add_destination_header (data->msg, data->target_uri); message_add_overwrite_header (data->msg, data->flags & G_FILE_COPY_OVERWRITE); g_vfs_backend_dav_send_async (backend, data->msg, try_move_do_cb, data); } static void try_move_target_delete_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); SoupMessage *msg = g_vfs_backend_dav_get_async_result_message (backend, result); CopyData *data = user_data; GInputStream *body; GError *error = NULL; guint status; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { g_vfs_job_failed_from_error (data->job, error); copy_data_free (data); g_object_unref (msg); g_clear_error (&error); return; } status = soup_message_get_status (msg); if (!SOUP_STATUS_IS_SUCCESSFUL (status)) { http_job_failed (data->job, msg); g_object_unref (msg); copy_data_free (data); return; } g_object_unref (body); g_object_unref (msg); try_move_do (backend, data); } static void try_move_source_stat_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); CopyData *data = user_data; GError *error = NULL; data->source_res = stat_location_finish (backend, &data->source_ft, &data->file_size, NULL, result, &error); if (data->target_res) { if (data->flags & G_FILE_COPY_OVERWRITE) { if (data->source_res) { if (data->target_ft == G_FILE_TYPE_DIRECTORY) { if (data->source_ft == G_FILE_TYPE_DIRECTORY) g_vfs_job_failed_literal (G_VFS_JOB(data->job), G_IO_ERROR, G_IO_ERROR_WOULD_MERGE, _("Can’t move directory over directory")); else g_vfs_job_failed_literal (G_VFS_JOB(data->job), G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("Can’t move over directory")); copy_data_free (data); return; } else if (data->source_ft == G_FILE_TYPE_DIRECTORY) { /* Overwriting a file with a directory, first remove the * file */ SoupMessage *msg; msg = soup_message_new_from_uri (SOUP_METHOD_DELETE, data->target_uri); dav_message_connect_signals (msg, backend); g_vfs_backend_dav_send_async (backend, msg, try_move_target_delete_cb, data); return; } } else { g_vfs_job_failed_from_error (data->job, error); copy_data_free (data); g_clear_error (&error); return; } } else { g_vfs_job_failed_literal (G_VFS_JOB(data->job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file exists")); copy_data_free (data); g_clear_error (&error); return; } } try_move_do (backend, data); } static void try_move_target_stat_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); CopyData *data = user_data; gboolean res; GError *error = NULL; res = stat_location_finish (backend, &data->target_ft, NULL, NULL, result, &error); if (!res && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_vfs_job_failed_from_error (data->job, error); g_clear_error (&error); copy_data_free (data); return; } g_clear_error (&error); data->target_res = res; stat_location_async (backend, data->source_uri, FALSE, try_move_source_stat_cb, data); } static gboolean try_move (GVfsBackend *backend, GVfsJobMove *job, const char *source, const char *destination, GFileCopyFlags flags, GFileProgressCallback progress_callback, gpointer progress_callback_data) { CopyData *data = g_slice_new0 (CopyData); data->job = G_VFS_JOB (job); data->flags = flags; data->progress_callback = progress_callback; data->progress_callback_data = progress_callback_data; if (flags & G_FILE_COPY_BACKUP) { if (flags & G_FILE_COPY_NO_FALLBACK_FOR_MOVE) { g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_CANT_CREATE_BACKUP, _("Backups not supported")); } else { /* Return G_IO_ERROR_NOT_SUPPORTED instead of G_IO_ERROR_CANT_CREATE_BACKUP * to be proceeded with copy and delete fallback (see g_file_move). */ g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Operation not supported"); } copy_data_free (data); return TRUE; } data->source_uri = g_vfs_backend_dav_uri_for_path (backend, source, FALSE); data->msg = soup_message_new_from_uri (SOUP_METHOD_MOVE, data->source_uri); data->target_uri = g_vfs_backend_dav_uri_for_path (backend, destination, FALSE); dav_message_connect_signals (data->msg, backend); stat_location_async (backend, data->target_uri, FALSE, try_move_target_stat_cb, data); return TRUE; } static void try_copy_do_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); CopyData *data = user_data; GInputStream *body; GError *error = NULL; guint status; body = g_vfs_backend_dav_send_finish (backend, result, &error); if (!body) { g_vfs_job_failed_from_error (data->job, error); copy_data_free (data); return; } /* See try_set_display_name () for the explanation of the PRECONDITION_FAILED * and IS_REDIRECTION handling below. */ status = soup_message_get_status (data->msg); if (SOUP_STATUS_IS_SUCCESSFUL (status)) { if (data->progress_callback) data->progress_callback (data->file_size, data->file_size, data->progress_callback_data); g_vfs_job_succeeded (data->job); } else if (status == SOUP_STATUS_PRECONDITION_FAILED || SOUP_STATUS_IS_REDIRECTION (status)) g_vfs_job_failed (data->job, G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); else http_job_failed (data->job, data->msg); g_object_unref (body); copy_data_free (data); } static void try_copy_target_stat_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); CopyData *data = user_data; GError *error = NULL; data->target_res = stat_location_finish (backend, &data->target_ft, NULL, NULL, result, &error); if (data->target_res) { if (data->flags & G_FILE_COPY_OVERWRITE) { if (data->target_ft == G_FILE_TYPE_DIRECTORY) { if (data->source_ft == G_FILE_TYPE_DIRECTORY) g_vfs_job_failed_literal (G_VFS_JOB(data->job), G_IO_ERROR, G_IO_ERROR_WOULD_MERGE, _("Can’t copy directory over directory")); else g_vfs_job_failed_literal (G_VFS_JOB(data->job), G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("File is directory")); copy_data_free (data); return; } } else { g_vfs_job_failed_literal (data->job, G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); copy_data_free (data); return; } } else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_vfs_job_failed_from_error (data->job, error); g_clear_error (&error); copy_data_free (data); return; } g_clear_error (&error); if (data->source_ft == G_FILE_TYPE_DIRECTORY) { g_vfs_job_failed_literal (data->job, G_IO_ERROR, G_IO_ERROR_WOULD_RECURSE, _("Can’t recursively copy directory")); copy_data_free (data); return; } data->msg = soup_message_new_from_uri (SOUP_METHOD_COPY, data->source_uri); message_add_destination_header (data->msg, data->target_uri); message_add_overwrite_header (data->msg, data->flags & G_FILE_COPY_OVERWRITE); dav_message_connect_signals (data->msg, backend); g_vfs_backend_dav_send_async (backend, data->msg, try_copy_do_cb, data); } static void try_copy_source_stat_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GVfsBackend *backend = G_VFS_BACKEND (source); CopyData *data = user_data; gboolean res; GError *error = NULL; res = stat_location_finish (backend, &data->source_ft, &data->file_size, NULL, result, &error); if (!res) { g_vfs_job_failed_from_error (data->job, error); g_clear_error (&error); copy_data_free (data); return; } g_clear_error (&error); data->source_res = res; stat_location_async (backend, data->target_uri, FALSE, try_copy_target_stat_cb, data); } static gboolean try_copy (GVfsBackend *backend, GVfsJobCopy *job, const char *source, const char *destination, GFileCopyFlags flags, GFileProgressCallback progress_callback, gpointer progress_callback_data) { CopyData *data = g_slice_new0 (CopyData); data->job = G_VFS_JOB (job); data->flags = flags; data->progress_callback = progress_callback; data->progress_callback_data = progress_callback_data; if (flags & G_FILE_COPY_BACKUP) { /* Return G_IO_ERROR_NOT_SUPPORTED instead of * G_IO_ERROR_CANT_CREATE_BACKUP to proceed with the GIO fallback * copy. */ g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Operation not supported"); copy_data_free (data); return TRUE; } data->source_uri = g_vfs_backend_dav_uri_for_path (backend, source, FALSE); data->target_uri = g_vfs_backend_dav_uri_for_path (backend, destination, FALSE); stat_location_async (backend, data->source_uri, FALSE, try_copy_source_stat_cb, data); return TRUE; } #define CHUNK_SIZE 65536 typedef struct { /* Job details */ GVfsBackend *backend; GVfsJob *job; GVfsJobPush *op_job; /* Local file */ GInputStream *in; goffset size; /* Remote file */ GUri *uri; SoupMessage *msg; goffset n_written; } PushHandle; static void push_handle_free (PushHandle *handle) { if (handle->in) { g_input_stream_close_async (handle->in, 0, NULL, NULL, NULL); g_object_unref (handle->in); } g_object_unref (handle->backend); g_object_unref (handle->job); g_clear_object (&handle->msg); g_uri_unref (handle->uri); g_slice_free (PushHandle, handle); } static void push_setup_message (PushHandle *handle) { message_add_overwrite_header (handle->msg, handle->op_job->flags & G_FILE_COPY_OVERWRITE); soup_message_headers_set_encoding (soup_message_get_request_headers (handle->msg), SOUP_ENCODING_CONTENT_LENGTH); soup_message_headers_set_content_length (soup_message_get_request_headers (handle->msg), handle->size); } static void push_restarted (SoupMessage *msg, gpointer user_data) { PushHandle *handle = user_data; handle->n_written = 0; g_object_set (msg, "method", SOUP_METHOD_PUT, NULL); push_setup_message (handle); soup_message_set_request_body (handle->msg, NULL, handle->in, handle->size); } static void push_wrote_body_data (SoupMessage *msg, guint chunk_size, gpointer user_data) { PushHandle *handle = user_data; handle->n_written += chunk_size; g_vfs_job_progress_callback (handle->n_written, handle->size, handle->job); } static void push_finished (SoupMessage *msg, gpointer user_data) { PushHandle *handle = user_data; if (g_vfs_job_is_finished (handle->job)) ; /* We got an error so we finished the job and cancelled msg. */ else if (!SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (msg))) http_job_failed (handle->job, msg); else { if (handle->op_job->remove_source) g_unlink (handle->op_job->local_path); g_vfs_job_succeeded (handle->job); } push_handle_free (handle); } static void push_done (GObject *source, GAsyncResult *result, gpointer user_data) { GInputStream *body; GVfsJob *job = G_VFS_JOB (user_data); GError *error = NULL; body = soup_session_send_finish (SOUP_SESSION (source), result, &error); if (!body) { g_vfs_job_failed_from_error (job, error); g_error_free (error); } else g_object_unref (body); } static void push_stat_dest_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GInputStream *body; PushHandle *handle = user_data; GFileType type; GError *error = NULL; body = soup_session_send_finish (SOUP_SESSION (source), result, &error); if (!body) { g_vfs_job_failed_from_error (handle->job, error); g_error_free (error); return; } if (stat_location_end (handle->msg, body, &type, NULL, NULL)) { if (!(handle->op_job->flags & G_FILE_COPY_OVERWRITE)) { g_object_unref (body); g_vfs_job_failed (handle->job, G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); push_handle_free (handle); return; } if (type == G_FILE_TYPE_DIRECTORY) { g_object_unref (body); g_vfs_job_failed (handle->job, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, _("File is directory")); push_handle_free (handle); return; } } g_object_unref (body); g_object_unref (handle->msg); handle->msg = soup_message_new_from_uri (SOUP_METHOD_PUT, handle->uri); push_setup_message (handle); soup_message_set_request_body (handle->msg, NULL, handle->in, handle->size); g_signal_connect (handle->msg, "restarted", G_CALLBACK (push_restarted), handle); g_signal_connect (handle->msg, "wrote-body-data", G_CALLBACK (push_wrote_body_data), handle); g_signal_connect (handle->msg, "finished", G_CALLBACK (push_finished), handle); dav_message_connect_signals (handle->msg, handle->backend); soup_session_send_async (G_VFS_BACKEND_HTTP (handle->backend)->session, handle->msg, G_PRIORITY_DEFAULT, NULL, push_done, handle->job); } static void push_source_fstat_cb (GObject *source, GAsyncResult *res, gpointer user_data) { GFileInputStream *fin = G_FILE_INPUT_STREAM (source); PushHandle *handle = user_data; GError *error = NULL; GFileInfo *info; info = g_file_input_stream_query_info_finish (fin, res, &error); if (info) { handle->size = g_file_info_get_size (info); g_object_unref (info); handle->msg = stat_location_start (handle->uri, FALSE); dav_message_connect_signals (handle->msg, handle->backend); soup_session_send_async (G_VFS_BACKEND_HTTP (handle->backend)->session, handle->msg, G_PRIORITY_DEFAULT, NULL, push_stat_dest_cb, handle); } else { g_vfs_job_failed_from_error (handle->job, error); g_error_free (error); push_handle_free (handle); } } static void push_source_open_cb (GObject *source, GAsyncResult *res, gpointer user_data) { GFile *source_file = G_FILE (source); PushHandle *handle = user_data; GError *error = NULL; GFileInputStream *fin; fin = g_file_read_finish (source_file, res, &error); if (fin) { handle->in = G_INPUT_STREAM (fin); g_file_input_stream_query_info_async (fin, G_FILE_ATTRIBUTE_STANDARD_SIZE, 0, handle->job->cancellable, push_source_fstat_cb, handle); } else { if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_IS_DIRECTORY) { /* Fall back to default implementation to improve the error message */ g_vfs_job_failed (handle->job, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported")); } else g_vfs_job_failed_from_error (handle->job, error); g_error_free (error); push_handle_free (handle); } } static void push_source_lstat_cb (GObject *source, GAsyncResult *res, gpointer user_data) { GFile *source_file = G_FILE (source); PushHandle *handle = user_data; GError *error = NULL; GFileInfo *info; info = g_file_query_info_finish (source_file, res, &error); if (!info) { g_vfs_job_failed_from_error (handle->job, error); g_error_free (error); push_handle_free (handle); return; } if ((handle->op_job->flags & G_FILE_COPY_NOFOLLOW_SYMLINKS) && g_file_info_get_file_type (info) == G_FILE_TYPE_SYMBOLIC_LINK) { /* Fall back to default implementation to copy symlink */ g_vfs_job_failed (handle->job, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported")); push_handle_free (handle); g_object_unref (info); return; } g_file_read_async (source_file, 0, handle->job->cancellable, push_source_open_cb, handle); g_object_unref (info); } static gboolean try_push (GVfsBackend *backend, GVfsJobPush *job, const char *destination, const char *local_path, GFileCopyFlags flags, gboolean remove_source, GFileProgressCallback progress_callback, gpointer progress_callback_data) { GFile *source; PushHandle *handle; if (remove_source && (flags & G_FILE_COPY_NO_FALLBACK_FOR_MOVE)) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported")); return TRUE; } handle = g_slice_new0 (PushHandle); handle->backend = g_object_ref (backend); handle->job = g_object_ref (G_VFS_JOB (job)); handle->op_job = job; handle->uri = g_vfs_backend_dav_uri_for_path (backend, destination, FALSE); source = g_file_new_for_path (local_path); g_file_query_info_async (source, G_FILE_ATTRIBUTE_STANDARD_TYPE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, 0, handle->job->cancellable, push_source_lstat_cb, handle); g_object_unref (source); return TRUE; } /* ************************************************************************* */ /* */ static void g_vfs_backend_dav_class_init (GVfsBackendDavClass *klass) { GObjectClass *gobject_class; GVfsBackendClass *backend_class; gobject_class = G_OBJECT_CLASS (klass); gobject_class->finalize = g_vfs_backend_dav_finalize; backend_class = G_VFS_BACKEND_CLASS (klass); backend_class->try_mount = try_mount; backend_class->try_query_info = try_query_info; backend_class->try_query_fs_info = try_query_fs_info; backend_class->try_enumerate = try_enumerate; backend_class->try_open_for_read = try_open_for_read; backend_class->try_create = try_create; backend_class->try_replace = try_replace; backend_class->try_write = try_write; backend_class->seek_on_write = do_seek_on_write; backend_class->truncate = do_truncate; backend_class->try_close_write = try_close_write; backend_class->try_make_directory = try_make_directory; backend_class->try_delete = try_delete; backend_class->try_set_display_name = try_set_display_name; backend_class->try_move = try_move; backend_class->try_copy = try_copy; backend_class->try_push = try_push; /* override the maximum number of connections, since the libsoup defaults * of 10 and 2 respectively are too low and may cause backend lockups when * a lot of files are opened at the same time */ http_try_init_session (32, 32); }