/*
* 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 "cockpitfsread.h"
#include "common/cockpitjson.h"
#include
#include
#include
#include
#include
#include
#define DEFAULT_MAX_READ_SIZE (16*1024*1024)
/**
* CockpitFsread:
*
* A #CockpitChannel that reads the content of a file.
*
* The payload type for this channel is 'fsread1'.
*/
#define COCKPIT_FSREAD(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSREAD, CockpitFsread))
typedef struct {
CockpitChannel parent;
const gchar *path;
int fd;
gchar *start_tag;
GQueue *queue;
guint idler;
} CockpitFsread;
typedef struct {
CockpitChannelClass parent_class;
} CockpitFsreadClass;
G_DEFINE_TYPE (CockpitFsread, cockpit_fsread, COCKPIT_TYPE_CHANNEL);
static gboolean
on_idle_send_block (gpointer data)
{
CockpitChannel *channel = data;
CockpitFsread *self = data;
const gchar *problem;
JsonObject *options;
GBytes *payload;
gchar *tag;
payload = g_queue_pop_head (self->queue);
if (payload == NULL)
{
self->idler = 0;
cockpit_channel_control (channel, "done", NULL);
problem = NULL;
if (self->fd >= 0 && self->start_tag)
{
tag = cockpit_get_file_tag_from_fd (self->fd);
if (g_strcmp0 (tag, self->start_tag) == 0)
{
options = cockpit_channel_close_options (channel);
json_object_set_string_member (options, "tag", tag);
}
else
{
problem = "change-conflict";
}
g_free (tag);
}
cockpit_channel_close (channel, problem);
return FALSE;
}
else
{
cockpit_channel_send (channel, payload, FALSE);
g_bytes_unref (payload);
return TRUE;
}
}
static void
cockpit_fsread_recv (CockpitChannel *channel,
GBytes *message)
{
cockpit_channel_fail (channel, "protocol-error", "received unexpected message in fsread channel");
}
static gchar *
file_tag_from_stat (int res,
int err,
struct stat *buf)
{
// The transaction tag is the inode and mtime of the file. Mtime is
// used to catch in-place modifications, and the inode to catch
// renames.
if (res >= 0)
return g_strdup_printf ("1:%lu-%lld.%ld",
(unsigned long)buf->st_ino,
(long long int)buf->st_mtim.tv_sec,
(long int)buf->st_mtim.tv_nsec);
else if (err == ENOENT)
return g_strdup ("-");
else
return NULL;
}
gchar *
cockpit_get_file_tag (const gchar *path)
{
struct stat buf;
int res = stat (path, &buf);
return file_tag_from_stat (res, errno, &buf);
}
gchar *
cockpit_get_file_tag_from_fd (int fd)
{
struct stat buf;
int res = fstat (fd, &buf);
return file_tag_from_stat (res, errno, &buf);
}
static void
cockpit_fsread_close (CockpitChannel *channel,
const gchar *problem)
{
CockpitFsread *self = COCKPIT_FSREAD (channel);
if (self->idler)
{
g_source_remove (self->idler);
self->idler = 0;
}
if (self->fd >= 0)
close (self->fd);
COCKPIT_CHANNEL_CLASS (cockpit_fsread_parent_class)->close (channel, problem);
}
static void
cockpit_fsread_init (CockpitFsread *self)
{
self->fd = -1;
}
static void
push_bytes (GQueue *queue,
GBytes *bytes)
{
gsize size;
gsize length;
gsize offset;
size = g_bytes_get_size (bytes);
if (size < 8192)
{
g_queue_push_tail (queue, bytes);
}
else
{
for (offset = 0; offset < size; offset += 4096)
{
length = MIN (4096, size - offset);
g_queue_push_tail (queue, g_bytes_new_from_bytes (bytes, offset, length));
}
g_bytes_unref (bytes);
}
}
static void
cockpit_fsread_prepare (CockpitChannel *channel)
{
CockpitFsread *self = COCKPIT_FSREAD (channel);
JsonObject *options;
gint64 max_read_size;
GMappedFile *mapped;
GBytes *bytes = NULL;
GError *error = NULL;
struct stat statbuf;
COCKPIT_CHANNEL_CLASS (cockpit_fsread_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 fsread channel");
return;
}
if (self->path == NULL || *(self->path) == 0)
{
cockpit_channel_fail (channel, "protocol-error", "missing \"path\" option for fsread channel");
return;
}
if (!cockpit_json_get_int (options, "max_read_size", DEFAULT_MAX_READ_SIZE, &max_read_size))
{
cockpit_channel_fail (channel, "protocol-error", "invalid \"max_read_size\" option for fsread channel");
return;
}
self->fd = open (self->path, O_RDONLY);
if (self->fd < 0)
{
int err = errno;
if (err == ENOENT)
{
options = cockpit_channel_close_options (channel);
json_object_set_string_member (options, "tag", "-");
cockpit_channel_close (channel, NULL);
}
else
{
if (err == EPERM || err == EACCES)
{
g_debug ("%s: couldn't open: %s", self->path, strerror (err));
cockpit_channel_close (channel, "access-denied");
}
else
{
cockpit_channel_fail (channel, "internal-error",
"%s: couldn't open: %s", self->path, strerror (err));
}
}
return;
}
if (fstat (self->fd, &statbuf) < 0)
{
cockpit_channel_fail (channel, "internal-error", "%s: couldn't stat: %s", self->path, strerror (errno));
return;
}
if ((statbuf.st_mode & S_IFMT) != S_IFREG)
{
cockpit_channel_fail (channel, "internal-error", "%s: not a regular file", self->path);
return;
}
if (statbuf.st_size > max_read_size)
{
cockpit_channel_close (channel, "too-large");
return;
}
mapped = g_mapped_file_new_from_fd (self->fd, FALSE, &error);
if (mapped)
{
bytes = g_mapped_file_get_bytes (mapped);
}
else if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NODEV))
{
gchar *contents;
gsize length;
g_clear_error (&error);
if (g_file_get_contents (self->path, &contents, &length, &error))
bytes = g_bytes_new_take (contents, length);
}
if (error)
{
cockpit_channel_fail (channel, "internal-error", "couldn't read: %s", error->message);
g_error_free (error);
return;
}
self->start_tag = cockpit_get_file_tag_from_fd (self->fd);
self->queue = g_queue_new ();
push_bytes (self->queue, bytes);
self->idler = g_idle_add (on_idle_send_block, self);
cockpit_channel_ready (channel, NULL);
if (mapped)
g_mapped_file_unref (mapped);
}
static void
cockpit_fsread_finalize (GObject *object)
{
CockpitFsread *self = COCKPIT_FSREAD (object);
g_free (self->start_tag);
if (self->queue)
{
while (!g_queue_is_empty (self->queue))
g_bytes_unref (g_queue_pop_head (self->queue));
g_queue_free (self->queue);
}
g_assert (self->idler == 0);
G_OBJECT_CLASS (cockpit_fsread_parent_class)->finalize (object);
}
static void
cockpit_fsread_class_init (CockpitFsreadClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
gobject_class->finalize = cockpit_fsread_finalize;
channel_class->prepare = cockpit_fsread_prepare;
channel_class->recv = cockpit_fsread_recv;
channel_class->close = cockpit_fsread_close;
}
/**
* cockpit_fsread_open:
* @transport: the transport to send/receive messages on
* @channel_id: the channel id
* @path: the path name of the file to read
*
* This function is mainly used by tests. The usual way
* to get a #CockpitFsread is via cockpit_channel_open()
*
* Returns: (transfer full): the new channel
*/
CockpitChannel *
cockpit_fsread_open (CockpitTransport *transport,
const gchar *channel_id,
const gchar *path)
{
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);
json_object_set_string_member (options, "payload", "fsread1");
channel = g_object_new (COCKPIT_TYPE_FSREAD,
"transport", transport,
"id", channel_id,
"options", options,
NULL);
json_object_unref (options);
return channel;
}