/* GIO - GLib Input, Output and Streaming Library * * Copyright (C) 2008 Red Hat, Inc. * * 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., 59 Temple Place, Suite 330, * Boston, MA 02111-1307, 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 "gvfsdaemonprotocol.h" #include "soup-input-stream.h" #include "soup-output-stream.h" typedef struct _MountAuthData MountAuthData; static void mount_auth_info_free (MountAuthData *info); typedef struct _AuthInfo { /* for server authentication */ char *username; char *password; char *realm; GPasswordSave pw_save; } AuthInfo; struct _MountAuthData { SoupSession *session; GMountSource *mount_source; AuthInfo server_auth; AuthInfo proxy_auth; }; struct _GVfsBackendDav { GVfsBackendHttp parent_instance; MountAuthData auth_info; }; 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); mount_auth_info_free (&(dav_backend->auth_info)); 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 inline gboolean sm_has_header (SoupMessage *msg, const char *header) { return soup_message_headers_get (msg->response_headers, 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 NULL; parent = g_strrstr_len (path, len, "/"); if (parent == NULL) return NULL; return g_strndup (path, (parent - path) + 1); } /* message utility functions */ static void message_add_destination_header (SoupMessage *msg, SoupURI *uri) { char *string; string = soup_uri_to_string (uri, FALSE); soup_message_headers_append (msg->request_headers, "Destination", string); g_free (string); } static void message_add_overwrite_header (SoupMessage *msg, gboolean overwrite) { soup_message_headers_append (msg->request_headers, "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 (msg->request_headers, "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[a_len - 1] == '/') a_len--; while (b[b_len - 1] == '/') b_len--; if (a_len == b_len) res = ! strncmp (a, b, a_len); else res = FALSE; return res; } /* Like soup_uri_equal */ static gboolean dav_uri_match (SoupURI *a, SoupURI *b, gboolean relax) { if (a->scheme != b->scheme || a->port != b->port || ! str_equal (a->user, b->user, FALSE) || ! str_equal (a->password, b->password, FALSE) || ! str_equal (a->host, b->host, TRUE) || ! path_equal (a->path, b->path, relax) || ! str_equal (a->query, b->query, FALSE) || ! str_equal (a->fragment, b->fragment, FALSE)) return FALSE; return TRUE; } static gboolean message_should_apply_redir_ref (SoupMessage *msg) { const char *header; header = soup_message_headers_get (msg->request_headers, "Apply-To-Redirect-Ref"); if (header == NULL || g_ascii_strcasecmp (header, "T")) return FALSE; return TRUE; } /* redirection */ static void redirect_handler (SoupMessage *msg, gpointer user_data) { SoupSession *session = user_data; const char *new_loc; SoupURI *new_uri; SoupURI *old_uri; guint status; gboolean redirect; status = msg->status_code; new_loc = soup_message_headers_get (msg->response_headers, "Location"); /* If we don't have a location to redirect to, just fail */ if (new_loc == NULL) return; new_uri = soup_uri_new_with_base (soup_message_get_uri (msg), new_loc); if (new_uri == NULL) { soup_message_set_status_full (msg, SOUP_STATUS_MALFORMED, "Invalid Redirect URL"); return; } old_uri = soup_message_get_uri (msg); /* 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 == TRUE) { const char *dest; dest = soup_message_headers_get (msg->request_headers, "Destination"); if (dest && g_str_has_suffix (dest, "/") == FALSE) { char *new_dest = g_strconcat (dest, "/", NULL); soup_message_headers_replace (msg->request_headers, "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) { /* Only corss-site redirect safe methods */ if (msg->method == SOUP_METHOD_GET && msg->method == SOUP_METHOD_HEAD && msg->method == SOUP_METHOD_OPTIONS && msg->method == SOUP_METHOD_PROPFIND) redirect = TRUE; } #if 0 else if (msg->status_code == SOUP_STATUS_SEE_OTHER || msg->status_code == SOUP_STATUS_FOUND) { /* Redirect using a GET */ g_object_set (msg, SOUP_MESSAGE_METHOD, SOUP_METHOD_GET, NULL); soup_message_set_request (msg, NULL, SOUP_MEMORY_STATIC, NULL, 0); soup_message_headers_set_encoding (msg->request_headers, SOUP_ENCODING_NONE); } #endif /* ELSE: * * Three possibilities: * * 1) This was a non-3xx response that happened to * have a "Location" header * 2) It's a non-redirecty 3xx response (300, 304, * 305, 306) * 3) It's some newly-defined 3xx response (308+) * * We ignore all of these cases. In the first two, * 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) { soup_message_set_uri (msg, new_uri); soup_session_requeue_message (session, msg); } soup_uri_free (new_uri); } static guint g_vfs_backend_dav_send_message (GVfsBackend *backend, SoupMessage *message) { GVfsBackendHttp *http_backend; SoupSession *session; http_backend = G_VFS_BACKEND_HTTP (backend); session = http_backend->session; /* We have our own custom redirect handler */ soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT); soup_message_add_header_handler (message, "got_body", "Location", G_CALLBACK (redirect_handler), session); return http_backend_send_message (backend, message); } /* ************************************************************************* */ /* 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; } } 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 xmlDocPtr parse_xml (SoupMessage *msg, xmlNodePtr *root, const char *name, GError **error) { xmlDocPtr doc; if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("HTTP Error: %s"), msg->reason_phrase); return NULL; } doc = xmlReadMemory (msg->response_body->data, msg->response_body->length, "response.xml", NULL, XML_PARSE_NOWARNING | XML_PARSE_NOBLANKS | XML_PARSE_NSCLEAN | XML_PARSE_NOCDATA | XML_PARSE_COMPACT); if (doc == NULL) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", _("Could not parse response")); return NULL; } *root = xmlDocGetRootElement (doc); if (*root == NULL || (*root)->children == NULL) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", _("Empty response")); return NULL; } if (strcmp ((char *) (*root)->name, name)) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", _("Unexpected reply from server")); 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; SoupURI *target; }; struct _MsResponse { Multistatus *multistatus; xmlNodePtr href; xmlNodePtr first_propstat; }; struct _MsPropstat { Multistatus *multistatus; xmlNodePtr prop_node; guint status_code; }; static gboolean multistatus_parse (SoupMessage *msg, Multistatus *multistatus, GError **error) { xmlDocPtr doc; xmlNodePtr root; doc = parse_xml (msg, &root, "multistatus", error); if (doc == NULL) return FALSE; multistatus->doc = doc; multistatus->root = root; multistatus->target = soup_message_get_uri (msg); return TRUE; } static void multistatus_free (Multistatus *multistatus) { xmlFreeDoc (multistatus->doc); } 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; 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; response->href = href; response->multistatus = multistatus; response->first_propstat = propstat; return resp_node != NULL; } static char * ms_response_get_basename (MsResponse *response) { const char *text; text = node_get_content (response->href); return http_uri_get_basename (text); } static gboolean ms_response_is_target (MsResponse *response) { const char *text; const char *path; SoupURI *target; SoupURI *uri; gboolean res; uri = NULL; path = NULL; target = response->multistatus->target; text = node_get_content (response->href); if (text == NULL) return FALSE; uri = soup_uri_new_with_base (target, text); if (uri == NULL) return FALSE; res = dav_uri_match (uri, target, TRUE); soup_uri_free (uri); return res; } 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) { 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; GTimeVal tv; GFileType file_type; char *mime_type; GIcon *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); file_type = G_FILE_TYPE_UNKNOWN; mime_type = NULL; 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)) continue; /* TODO: check namespace, parse user data nodes*/ text = node_get_content (node); if (node_has_name (node, "resourcetype")) { file_type = parse_resourcetype (node); g_file_info_set_file_type (info, file_type); } else if (node_has_name (node, "displayname")) { g_file_info_set_display_name (info, text); } 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")) { if (! g_time_val_from_iso8601 (text, &tv)) continue; g_file_info_set_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CREATED, tv.tv_sec); } else if (node_has_name (node, "getcontenttype")) { mime_type = g_strdup (text); } 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")) { if (g_time_val_from_iso8601 (text, &tv)) g_file_info_set_modification_time (info, &tv); } } } if (file_type == G_FILE_TYPE_DIRECTORY) { icon = g_themed_icon_new ("folder"); file_info_set_content_type (info, "inode/directory"); } else { if (mime_type == NULL) mime_type = g_content_type_guess (basename, NULL, 0, NULL); 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"); file_info_set_content_type (info, mime_type); } if (have_display_name == FALSE) g_file_info_set_display_name (info, basename); g_file_info_set_icon (info, icon); g_object_unref (icon); g_free (mime_type); g_free (basename); } static GFileType ms_response_to_file_type (MsResponse *response) { xmlNodeIter prop_iter; MsPropstat propstat; GFileType file_type; guint status; file_type = G_FILE_TYPE_UNKNOWN; ms_response_get_propstat_iter (response, &prop_iter); while (xml_node_iter_next (&prop_iter)) { xmlNodePtr iter; status = ms_response_get_propstat (&prop_iter, &propstat); if (! SOUP_STATUS_IS_SUCCESSFUL (status)) continue; for (iter = propstat.prop_node->children; iter; iter = iter->next) { if (node_is_element (iter) && node_has_name_ns (iter, "resourcetype", "DAV:")) break; } if (iter) { file_type = parse_resourcetype (iter); break; } } return file_type; } #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 SoupMessage * propfind_request_new (GVfsBackend *backend, const char *filename, guint depth, const PropName *properties) { SoupMessage *msg; SoupURI *uri; const char *header_depth; GString *body; uri = http_backend_uri_for_filename (backend, filename, depth > 0); msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri); soup_uri_free (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 (msg->request_headers, "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); soup_message_set_request (msg, "application/xml", SOUP_MEMORY_TAKE, body->str, body->len); g_string_free (body, FALSE); return msg; } static SoupMessage * stat_location_begin (SoupURI *uri, gboolean count_children) { SoupMessage *msg; const char *depth; static const char *stat_profind_body = PROPSTAT_XML_BEGIN PROPSTAT_XML_PROP_BEGIN "\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 (msg->request_headers, "Depth", depth); soup_message_set_request (msg, "application/xml", SOUP_MEMORY_STATIC, stat_profind_body, strlen (stat_profind_body)); return msg; } static gboolean stat_location_finish (SoupMessage *msg, GFileType *target_type, guint *num_children) { Multistatus ms; xmlNodeIter iter; gboolean res; GError *error; guint child_count; GFileType file_type; if (msg->status_code != 207) return FALSE; res = multistatus_parse (msg, &ms, &error); if (res == FALSE) return FALSE; res = FALSE; child_count = 0; file_type = G_FILE_TYPE_UNKNOWN; multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; if (! multistatus_get_response (&iter, &response)) continue; if (ms_response_is_target (&response)) { file_type = ms_response_to_file_type (&response); res = TRUE; } else child_count++; } if (res) { if (target_type) *target_type = file_type; if (num_children) *num_children = child_count; } multistatus_free (&ms); return res; } static gboolean stat_location (GVfsBackend *backend, SoupURI *uri, GFileType *target_type, guint *num_children, GError **error) { SoupMessage *msg; guint status; gboolean count_children; gboolean res; count_children = num_children != NULL; msg = stat_location_begin (uri, count_children); if (msg == NULL) return FALSE; status = g_vfs_backend_dav_send_message (backend, msg); if (status != 207) { g_set_error (error, G_IO_ERROR, http_error_code_from_status (status), "%s", msg->reason_phrase); return FALSE; } res = stat_location_finish (msg, target_type, num_children); if (res == FALSE) g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Response invalid")); return res; } /* ************************************************************************* */ /* 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 void soup_authenticate_from_data (SoupSession *session, SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data) { MountAuthData *data; AuthInfo *info; g_print ("+ soup_authenticate_from_data (%s) \n", retrying ? "retrying" : "first auth"); if (retrying) return; data = (MountAuthData *) user_data; if (soup_auth_is_for_proxy (auth)) info = &data->proxy_auth; else info = &data->server_auth; soup_auth_authenticate (auth, info->username, info->password); } static void soup_authenticate_interactive (SoupSession *session, 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; g_print ("+ soup_authenticate_interactive (%s) \n", retrying ? "retrying" : "first auth"); data = (MountAuthData *) user_data; new_username = NULL; new_password = NULL; realm = NULL; pw_ask_flags = G_ASK_PASSWORD_NEED_PASSWORD; is_proxy = soup_auth_is_for_proxy (auth); realm = soup_auth_get_realm (auth); if (is_proxy) info = &(data->proxy_auth); else info = &(data->server_auth); if (realm && info->realm == NULL) info->realm = g_strdup (realm); else if (realm && info->realm && !g_str_equal (realm, info->realm)) return; have_auth = info->username && info->password; if (have_auth == FALSE && g_vfs_keyring_is_available ()) { SoupURI *uri; SoupURI *uri_free = NULL; pw_ask_flags |= G_ASK_PASSWORD_SAVING_SUPPORTED; if (is_proxy) { g_object_get (session, SOUP_SESSION_PROXY_URI, &uri_free, NULL); uri = uri_free; } else uri = soup_message_get_uri (msg); res = g_vfs_keyring_lookup_password (info->username, uri->host, NULL, "http", realm, is_proxy ? "proxy" : "basic", uri->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 (uri_free) soup_uri_free (uri_free); } if (retrying == FALSE && have_auth) { soup_auth_authenticate (auth, info->username, info->password); return; } if (is_proxy == FALSE) { if (realm == NULL) realm = _("WebDAV share"); prompt = g_strdup_printf (_("Enter password for %s"), realm); } else prompt = g_strdup (_("Please enter 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) { 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_session_cancel_message (session, msg, SOUP_STATUS_CANCELLED); g_print ("- soup_authenticate \n"); g_free (prompt); } static void keyring_save_authinfo (AuthInfo *info, SoupURI *uri, gboolean is_proxy) { const char *type = is_proxy ? "proxy" : "basic"; g_vfs_keyring_save_password (info->username, uri->host, NULL, "http", info->realm, type, uri->port, info->password, info->pw_save); } /* ************************************************************************* */ static SoupURI * g_mount_spec_to_dav_uri (GMountSpec *spec) { SoupURI *uri; const char *host; const char *user; const char *port; const char *ssl; gint port_num; host = g_mount_spec_get (spec, "host"); user = g_mount_spec_get (spec, "user"); port = g_mount_spec_get (spec, "port"); ssl = g_mount_spec_get (spec, "ssl"); if (host == NULL || *host == 0) return NULL; uri = soup_uri_new (NULL); if (ssl != NULL && (strcmp (ssl, "true") == 0)) soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTPS); else soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTP); soup_uri_set_user (uri, user); if (port && (port_num = atoi (port))) soup_uri_set_port (uri, port_num); soup_uri_set_host (uri, host); soup_uri_set_path (uri, spec->mount_prefix); return uri; } static GMountSpec * g_mount_spec_from_dav_uri (SoupURI *uri) { GMountSpec *spec; const char *ssl; spec = g_mount_spec_new ("dav"); g_mount_spec_set (spec, "host", uri->host); if (uri->scheme == SOUP_URI_SCHEME_HTTPS) ssl = "true"; else ssl = "false"; g_mount_spec_set (spec, "ssl", ssl); if (uri->user) g_mount_spec_set (spec, "user", uri->user); if (! soup_uri_uses_default_port (uri)) { char *port = g_strdup_printf ("%u", uri->port); g_mount_spec_set (spec, "port", port); g_free (port); } g_mount_spec_set_mount_prefix (spec, uri->path); return spec; } /* ************************************************************************* */ /* Backend Functions */ static void do_mount (GVfsBackend *backend, GVfsJobMount *job, GMountSpec *mount_spec, GMountSource *mount_source, gboolean is_automount) { MountAuthData *data; SoupSession *session; SoupMessage *msg_opts; SoupMessage *msg_stat; SoupURI *mount_base; gulong signal_id; guint status; gboolean is_success; gboolean is_webdav; gboolean res; char *last_good_path; char *display_name; g_print ("+ mount\n"); mount_base = g_mount_spec_to_dav_uri (mount_spec); if (mount_base == NULL) { g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Invalid mount spec")); return; } session = G_VFS_BACKEND_HTTP (backend)->session; G_VFS_BACKEND_HTTP (backend)->mount_base = mount_base; data = &(G_VFS_BACKEND_DAV (backend)->auth_info); data->mount_source = g_object_ref (mount_source); data->server_auth.username = g_strdup (mount_base->user); data->server_auth.pw_save = G_PASSWORD_SAVE_NEVER; data->proxy_auth.pw_save = G_PASSWORD_SAVE_NEVER; signal_id = g_signal_connect (session, "authenticate", G_CALLBACK (soup_authenticate_interactive), data); last_good_path = NULL; msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, mount_base); msg_stat = stat_location_begin (mount_base, FALSE); do { status = g_vfs_backend_dav_send_message (backend, msg_opts); is_success = SOUP_STATUS_IS_SUCCESSFUL (status); is_webdav = is_success && sm_has_header (msg_opts, "DAV"); soup_message_headers_clear (msg_opts->response_headers); soup_message_body_truncate (msg_opts->response_body); if (is_webdav) { GFileType file_type; SoupURI *cur_uri; cur_uri = soup_message_get_uri (msg_opts); soup_message_set_uri (msg_stat, cur_uri); g_vfs_backend_dav_send_message (backend, msg_stat); res = stat_location_finish (msg_stat, &file_type, NULL); if (res && file_type == G_FILE_TYPE_DIRECTORY) { g_free (last_good_path); last_good_path = mount_base->path; } mount_base->path = path_get_parent_dir (mount_base->path); soup_message_set_uri (msg_opts, mount_base); soup_message_headers_clear (msg_stat->response_headers); soup_message_body_truncate (msg_stat->response_body); } } while (is_webdav && mount_base->path != NULL); /* we have reached the end of paths we are allowed to * chdir up to (or couldn't chdir up at all) */ /* check if we at all have a good path */ if (last_good_path == NULL) { /* TODO: set correct error in case of cancellation */ if (!is_success) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("HTTP Error: %s"), msg_opts->reason_phrase); else if (!is_webdav) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Not a WebDAV enabled share")); else g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_FAILED, _("Not a WebDAV enabled share")); /* TODO: We leak a bunch of stuff here :-( */ /* TODO: STRING CHANGE: change to: Could not find an enclosing directory */ return; } /* Success! We are mounted */ /* Save the auth info in the keyring */ keyring_save_authinfo (&(data->server_auth), mount_base, FALSE); /* TODO: save proxy auth */ /* Set the working path in mount path */ g_free (mount_base->path); mount_base->path = last_good_path; /* dup the mountspec, but only copy known fields */ mount_spec = g_mount_spec_from_dav_uri (mount_base); g_vfs_backend_set_mount_spec (backend, mount_spec); g_vfs_backend_set_icon_name (backend, "folder-remote"); display_name = g_strdup_printf (_("WebDAV on %s"), mount_base->host); g_vfs_backend_set_display_name (backend, display_name); g_free (display_name); /* cleanup */ g_mount_spec_unref (mount_spec); g_object_unref (msg_opts); g_object_unref (msg_stat); /* switch the signal handler */ g_signal_handler_disconnect (session, signal_id); g_signal_connect (session, "authenticate", G_CALLBACK (soup_authenticate_from_data), data); /* also auth the workaround async session we need for SoupInputStream */ g_signal_connect (G_VFS_BACKEND_HTTP (backend)->session_async, "authenticate", G_CALLBACK (soup_authenticate_from_data), data); g_vfs_job_succeeded (G_VFS_JOB (job)); g_print ("- mount\n"); } static PropName ls_propnames[] = { {"creationdate", NULL}, {"displayname", NULL}, {"getcontentlength", NULL}, {"getcontenttype", NULL}, {"getetag", NULL}, {"getlastmodified", NULL}, {"resourcetype", NULL}, {NULL, NULL} }; /* *** query_info () *** */ static void do_query_info (GVfsBackend *backend, GVfsJobQueryInfo *job, const char *filename, GFileQueryInfoFlags flags, GFileInfo *info, GFileAttributeMatcher *matcher) { SoupMessage *msg; Multistatus ms; xmlNodeIter iter; gboolean res; GError *error; error = NULL; g_print ("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; } message_add_redirect_header (msg, flags); g_vfs_backend_dav_send_message (backend, msg); res = multistatus_parse (msg, &ms, &error); if (res == FALSE) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); return; } res = FALSE; multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; if (! multistatus_get_response (&iter, &response)) continue; if (ms_response_is_target (&response)) { ms_response_to_file_info (&response, job->file_info); res = TRUE; } } 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")); } /* *** enumerate *** */ static void do_enumerate (GVfsBackend *backend, GVfsJobEnumerate *job, const char *filename, GFileAttributeMatcher *matcher, GFileQueryInfoFlags flags) { SoupMessage *msg; Multistatus ms; xmlNodeIter iter; gboolean res; GError *error; error = NULL; g_print ("+ do_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; } message_add_redirect_header (msg, flags); g_vfs_backend_dav_send_message (backend, msg); res = multistatus_parse (msg, &ms, &error); if (res == FALSE) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (error); g_object_unref (msg); return; } multistatus_get_response_iter (&ms, &iter); while (xml_node_iter_next (&iter)) { MsResponse response; GFileInfo *info; if (! multistatus_get_response (&iter, &response)) continue; if (ms_response_is_target (&response)) continue; info = g_file_info_new (); ms_response_to_file_info (&response, info); g_vfs_job_enumerate_add_info (job, info); } multistatus_free (&ms); g_object_unref (msg); g_vfs_job_succeeded (G_VFS_JOB (job)); /* should that be called earlier? */ g_vfs_job_enumerate_done (G_VFS_JOB_ENUMERATE (job)); } /* ************************************************************************* */ /* */ /* *** create () *** */ static void try_create_tested_existence (SoupSession *session, SoupMessage *msg, gpointer user_data) { GVfsJob *job = G_VFS_JOB (user_data); GVfsBackendHttp *op_backend = job->backend_data; GOutputStream *stream; SoupMessage *put_msg; SoupURI *uri; if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) { g_vfs_job_failed (job, G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); return; } /* TODO: other errors */ 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 (put_msg->request_headers, "If-None-Match", "*"); */ stream = soup_output_stream_new (op_backend->session, put_msg, -1); g_object_unref (put_msg); g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream); g_vfs_job_succeeded (job); } static gboolean try_create (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, GFileCreateFlags flags) { SoupMessage *msg; SoupURI *uri; /* TODO: if SoupOutputStream supported chunked requests, we could * use a PUT with "If-None-Match: *" and "Expect: 100-continue" */ uri = http_backend_uri_for_filename (backend, filename, FALSE); msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri); soup_uri_free (uri); g_vfs_job_set_backend_data (G_VFS_JOB (job), backend, NULL); http_backend_queue_message (backend, msg, try_create_tested_existence, job); return TRUE; } /* *** replace () *** */ static void open_for_replace_succeeded (GVfsBackendHttp *op_backend, GVfsJob *job, SoupURI *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 (put_msg->request_headers, "If-Match", etag); stream = soup_output_stream_new (op_backend->session, put_msg, -1); g_object_unref (put_msg); g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream); g_vfs_job_succeeded (job); } static void try_replace_checked_etag (SoupSession *session, SoupMessage *msg, gpointer user_data) { GVfsJob *job = G_VFS_JOB (user_data); GVfsBackendHttp *op_backend = job->backend_data; if (msg->status_code == SOUP_STATUS_PRECONDITION_FAILED) { 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 */ open_for_replace_succeeded (op_backend, job, soup_message_get_uri (msg), soup_message_headers_get (msg->request_headers, "If-Match")); } static gboolean try_replace (GVfsBackend *backend, GVfsJobOpenForWrite *job, const char *filename, const char *etag, gboolean make_backup, GFileCreateFlags flags) { GVfsBackendHttp *op_backend; SoupURI *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 = http_backend_uri_for_filename (backend, filename, FALSE); if (etag) { SoupMessage *msg; msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri); soup_uri_free (uri); soup_message_headers_append (msg->request_headers, "If-Match", etag); g_vfs_job_set_backend_data (G_VFS_JOB (job), op_backend, NULL); soup_session_queue_message (op_backend->session, msg, try_replace_checked_etag, job); return TRUE; } open_for_replace_succeeded (op_backend, G_VFS_JOB (job), uri, NULL); soup_uri_free (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; } /* *** close_write () *** */ static void close_write_ready (GObject *source_object, GAsyncResult *result, gpointer user_data) { GOutputStream *stream; GVfsJob *job; GError *error; gboolean res; error = NULL; job = G_VFS_JOB (user_data); stream = G_OUTPUT_STREAM (source_object); res = g_output_stream_close_finish (stream, result, &error); if (res == FALSE) { g_vfs_job_failed_literal (G_VFS_JOB (job), error->domain, error->code, error->message); g_error_free (error); } else g_vfs_job_succeeded (job); g_object_unref (stream); } static gboolean try_close_write (GVfsBackend *backend, GVfsJobCloseWrite *job, GVfsBackendHandle handle) { GOutputStream *stream; stream = G_OUTPUT_STREAM (handle); g_output_stream_close_async (stream, G_PRIORITY_DEFAULT, G_VFS_JOB (job)->cancellable, close_write_ready, job); return TRUE; } static void do_make_directory (GVfsBackend *backend, GVfsJobMakeDirectory *job, const char *filename) { SoupMessage *msg; SoupURI *uri; guint status; uri = http_backend_uri_for_filename (backend, filename, TRUE); msg = soup_message_new_from_uri (SOUP_METHOD_MKCOL, uri); soup_uri_free (uri); status = g_vfs_backend_dav_send_message (backend, 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 g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, http_error_code_from_status (status), msg->reason_phrase); else g_vfs_job_succeeded (G_VFS_JOB (job)); g_object_unref (msg); } static void do_delete (GVfsBackend *backend, GVfsJobDelete *job, const char *filename) { SoupMessage *msg; SoupURI *uri; GFileType file_type; gboolean res; guint num_children; guint status; GError *error; error = NULL; uri = http_backend_uri_for_filename (backend, filename, FALSE); res = stat_location (backend, uri, &file_type, &num_children, &error); if (res == FALSE) { g_vfs_job_failed_from_error (G_VFS_JOB (job), error); g_error_free (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); status = g_vfs_backend_dav_send_message (backend, msg); if (!SOUP_STATUS_IS_SUCCESSFUL (status)) g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, http_error_code_from_status (status), msg->reason_phrase); else g_vfs_job_succeeded (G_VFS_JOB (job)); soup_uri_free (uri); g_object_unref (msg); } static void do_set_display_name (GVfsBackend *backend, GVfsJobSetDisplayName *job, const char *filename, const char *display_name) { SoupMessage *msg; SoupURI *source; SoupURI *target; char *target_path; char *dirname; guint status; source = http_backend_uri_for_filename (backend, filename, FALSE); msg = soup_message_new_from_uri (SOUP_METHOD_MOVE, source); dirname = g_path_get_dirname (filename); target_path = g_build_filename (dirname, display_name, NULL); target = http_backend_uri_for_filename (backend, target_path, FALSE); message_add_destination_header (msg, target); message_add_overwrite_header (msg, FALSE); status = g_vfs_backend_dav_send_message (backend, 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_print ("new target_path: %s\n", target_path); g_vfs_job_set_display_name_set_new_path (job, target_path); g_vfs_job_succeeded (G_VFS_JOB (job)); } else if (status == SOUP_STATUS_PRECONDITION_FAILED || SOUP_STATUS_IS_REDIRECTION (status)) g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, G_IO_ERROR_EXISTS, _("Target file already exists")); else g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, http_error_code_from_status (status), "%s", msg->reason_phrase); g_object_unref (msg); g_free (dirname); g_free (target_path); soup_uri_free (target); soup_uri_free (source); } static gboolean try_unmount (GVfsBackend *backend, GVfsJobUnmount *job) { _exit (0); } /* ************************************************************************* */ /* */ 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 = NULL; backend_class->mount = do_mount; backend_class->try_query_info = NULL; backend_class->query_info = do_query_info; backend_class->enumerate = do_enumerate; backend_class->try_create = try_create; backend_class->try_replace = try_replace; backend_class->try_write = try_write; backend_class->try_close_write = try_close_write; backend_class->make_directory = do_make_directory; backend_class->delete = do_delete; backend_class->set_display_name = do_set_display_name; backend_class->try_unmount = try_unmount; }