/* * This file is part of Cockpit. * * Copyright (C) 2014 Red Hat, Inc. * * Cockpit 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.1 of the License, or * (at your option) any later version. * * Cockpit 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 Cockpit; If not, see . */ #include "config.h" #include "cockpitfsreplace.h" #include "cockpitfsread.h" #include "common/cockpitjson.h" #include #include #include #include #include #include #include /** * CockpitFsreplace: * * A #CockpitChannel that writes/replaces the content of a file. * * The payload type for this channel is 'fsreplace1'. */ #define COCKPIT_FSREPLACE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSREPLACE, CockpitFsreplace)) typedef struct { CockpitChannel parent; const gchar *path; gchar *tmp_path; int fd; gboolean got_content; const gchar *expected_tag; guint sig_close; } CockpitFsreplace; typedef struct { CockpitChannelClass parent_class; } CockpitFsreplaceClass; G_DEFINE_TYPE (CockpitFsreplace, cockpit_fsreplace, COCKPIT_TYPE_CHANNEL); static void close_with_errno (CockpitFsreplace *self, const gchar *diagnostic, int err) { CockpitChannel *channel = COCKPIT_CHANNEL (self); if (err == EPERM || err == EACCES) { g_debug ("%s: %s: %s", self->path, diagnostic, strerror (err)); cockpit_channel_close (channel, "access-denied"); } else { cockpit_channel_fail (channel, "internal-error", "%s: %s: %s", self->path, diagnostic, strerror (err)); } } static void cockpit_fsreplace_recv (CockpitChannel *channel, GBytes *message) { CockpitFsreplace *self = COCKPIT_FSREPLACE (channel); gsize size; const char *data = g_bytes_get_data (message, &size); self->got_content = TRUE; while (size > 0) { ssize_t n = write (self->fd, data, size); if (n < 0) { if (errno == EINTR) continue; close_with_errno (self, "couldn't write", errno); return; } g_return_if_fail (n > 0); size -= n; data += n; } } static int xfsync (int fd) { while (TRUE) { int res = fsync (fd); if (res < 0 && errno == EINTR) continue; return res; } } static int xclose (int fd) { /* http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html */ int res = close (fd); if (res < 0 && errno == EINTR) return 0; else return res; } static gboolean cockpit_fsreplace_control (CockpitChannel *channel, const gchar *command, JsonObject *unused) { CockpitFsreplace *self = COCKPIT_FSREPLACE (channel); gchar *actual_tag = NULL; gchar *new_tag = NULL; JsonObject *options; if (!g_str_equal (command, "done")) return FALSE; g_object_ref (self); /* Commit the changes when there was no problem */ if (xfsync (self->fd) < 0 || xclose (self->fd) < 0) { close_with_errno (self, "couldn't sync", errno); goto out; } else { actual_tag = cockpit_get_file_tag (self->path); if (self->expected_tag && g_strcmp0 (self->expected_tag, actual_tag)) { cockpit_channel_close (channel, "out-of-date"); goto out; } else { options = cockpit_channel_close_options (channel); if (!self->got_content) { json_object_set_string_member (options, "tag", "-"); if (unlink (self->tmp_path) < 0) g_message ("%s: couldn't remove temp file: %s", self->tmp_path, g_strerror (errno)); if (unlink (self->path) < 0 && errno != ENOENT) { close_with_errno (self, "couldn't unlink", errno); goto out; } } else { new_tag = cockpit_get_file_tag (self->tmp_path); json_object_set_string_member (options, "tag", new_tag); if (rename (self->tmp_path, self->path) < 0) { close_with_errno (self, "couldn't rename", errno); goto out; } } } } cockpit_channel_close (channel, NULL); out: g_free (new_tag); g_free (actual_tag); self->fd = -1; g_object_unref (self); return TRUE; } static void cockpit_fsreplace_close (CockpitChannel *channel, const gchar *problem) { CockpitFsreplace *self = COCKPIT_FSREPLACE (channel); if (self->fd != -1) close (self->fd); self->fd = -1; /* Cleanup in case of problem */ if (problem) { if (self->tmp_path) if (unlink (self->tmp_path) < 0 && errno != ENOENT) g_message ("%s: couldn't remove temp file: %s", self->tmp_path, g_strerror (errno)); } COCKPIT_CHANNEL_CLASS (cockpit_fsreplace_parent_class)->close (channel, problem); } static void cockpit_fsreplace_init (CockpitFsreplace *self) { self->fd = -1; } static void cockpit_fsreplace_prepare (CockpitChannel *channel) { CockpitFsreplace *self = COCKPIT_FSREPLACE (channel); JsonObject *options; gchar *actual_tag = NULL; COCKPIT_CHANNEL_CLASS (cockpit_fsreplace_parent_class)->prepare (channel); options = cockpit_channel_get_options (channel); if (!cockpit_json_get_string (options, "path", NULL, &self->path)) { cockpit_channel_fail (channel, "protocol-error", "invalid \"path\" option for fsreplace1 channel"); goto out; } else if (self->path == NULL || g_str_equal (self->path, "")) { cockpit_channel_fail (channel, "protocol-error", "missing \"path\" option for fsreplace1 channel"); goto out; } if (!cockpit_json_get_string (options, "tag", NULL, &self->expected_tag)) { cockpit_channel_fail (channel, "protocol-error", "%s: invalid \"tag\" option for fsreplace1 channel", self->path); goto out; } actual_tag = cockpit_get_file_tag (self->path); if (self->expected_tag && g_strcmp0 (self->expected_tag, actual_tag)) { cockpit_channel_close (channel, "change-conflict"); goto out; } // TODO - delay the opening until the first content message. That // way, we don't create a useless temporary file (which might even // fail). for (int i = 1; i < 10000; i++) { self->tmp_path = g_strdup_printf ("%s.%d", self->path, i); self->fd = open (self->tmp_path, O_WRONLY | O_CREAT | O_EXCL, 0666); if (self->fd >= 0 || errno != EEXIST) break; g_free (self->tmp_path); self->tmp_path = NULL; } if (self->fd < 0) close_with_errno (self, "couldn't open unique file", errno); else cockpit_channel_ready (channel, NULL); out: g_free (actual_tag); } static void cockpit_fsreplace_finalize (GObject *object) { CockpitFsreplace *self = COCKPIT_FSREPLACE (object); g_free (self->tmp_path); G_OBJECT_CLASS (cockpit_fsreplace_parent_class)->finalize (object); } static void cockpit_fsreplace_class_init (CockpitFsreplaceClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass); gobject_class->finalize = cockpit_fsreplace_finalize; channel_class->prepare = cockpit_fsreplace_prepare; channel_class->control = cockpit_fsreplace_control; channel_class->recv = cockpit_fsreplace_recv; channel_class->close = cockpit_fsreplace_close; } /** * cockpit_fsreplace_open: * @transport: the transport to send/receive messages on * @channel_id: the channel id * @path: the path name of the file to write * @tag: the expected tag, or NULL * * This function is mainly used by tests. The usual way * to get a #CockpitFsreplace is via cockpit_channel_open() * * Returns: (transfer full): the new channel */ CockpitChannel * cockpit_fsreplace_open (CockpitTransport *transport, const gchar *channel_id, const gchar *path, const gchar *tag) { CockpitChannel *channel; JsonObject *options; g_return_val_if_fail (channel_id != NULL, NULL); options = json_object_new (); json_object_set_string_member (options, "path", path); if (tag) json_object_set_string_member (options, "tag", tag); json_object_set_string_member (options, "payload", "fsreplace1"); channel = g_object_new (COCKPIT_TYPE_FSREPLACE, "transport", transport, "id", channel_id, "options", options, NULL); json_object_unref (options); return channel; }