"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "common/flatpak-exports.c" between
flatpak-1.15.1.tar.xz and flatpak-1.15.2.tar.xz

About: Flatpak is a Linux application sandboxing and distribution framework. Pre-release.

flatpak-exports.c  (flatpak-1.15.1.tar.xz):flatpak-exports.c  (flatpak-1.15.2.tar.xz)
skipping to change at line 55 skipping to change at line 55
#include "flatpak-utils-base-private.h" #include "flatpak-utils-base-private.h"
#include "flatpak-dir-private.h" #include "flatpak-dir-private.h"
#include "flatpak-systemd-dbus-generated.h" #include "flatpak-systemd-dbus-generated.h"
#include "flatpak-error.h" #include "flatpak-error.h"
/* We don't want to export paths pointing into these, because they are readonly /* We don't want to export paths pointing into these, because they are readonly
(so we can't create mountpoints there) and don't match what's on the host any way. (so we can't create mountpoints there) and don't match what's on the host any way.
flatpak_abs_usrmerged_dirs get the same treatment without having to be listed flatpak_abs_usrmerged_dirs get the same treatment without having to be listed
here. */ here. */
const char *dont_export_in[] = { const char *dont_export_in[] = {
"/usr", "/etc", "/app", "/dev", "/proc", NULL "/.flatpak-info",
"/app",
"/dev",
"/etc",
"/proc",
"/run/flatpak",
"/run/host",
"/usr",
NULL
}; };
static char * static char *
make_relative (const char *base, const char *path) make_relative (const char *base, const char *path)
{ {
GString *s = g_string_new (""); GString *s = g_string_new ("");
while (*base != 0) while (*base != 0)
{ {
while (*base == '/') while (*base == '/')
skipping to change at line 431 skipping to change at line 439
g_autofree const char **keys = (const char **) g_hash_table_get_keys_as_array (exports->hash, &n_keys); g_autofree const char **keys = (const char **) g_hash_table_get_keys_as_array (exports->hash, &n_keys);
g_autoptr(GList) eps = NULL; g_autoptr(GList) eps = NULL;
GList *l; GList *l;
struct stat buf; struct stat buf;
eps = g_hash_table_get_values (exports->hash); eps = g_hash_table_get_values (exports->hash);
eps = g_list_sort (eps, (GCompareFunc) compare_eps); eps = g_list_sort (eps, (GCompareFunc) compare_eps);
g_qsort_with_data (keys, n_keys, sizeof (char *), (GCompareDataFunc) flatpak_s trcmp0_ptr, NULL); g_qsort_with_data (keys, n_keys, sizeof (char *), (GCompareDataFunc) flatpak_s trcmp0_ptr, NULL);
flatpak_debug2 ("Converting FlatpakExports to bwrap arguments..."); g_debug ("Converting FlatpakExports to bwrap arguments...");
for (l = eps; l != NULL; l = l->next) for (l = eps; l != NULL; l = l->next)
{ {
ExportedPath *ep = l->data; ExportedPath *ep = l->data;
const char *path = ep->path; const char *path = ep->path;
g_assert (is_export_mode (ep->mode)); g_assert (is_export_mode (ep->mode));
if (ep->mode == FAKE_MODE_SYMLINK) if (ep->mode == FAKE_MODE_SYMLINK)
{ {
flatpak_debug2 ("\"%s\" is meant to be a symlink", path); g_debug ("\"%s\" is meant to be a symlink", path);
if (path_parent_is_mapped (keys, n_keys, exports->hash, path)) if (path_parent_is_mapped (keys, n_keys, exports->hash, path))
{ {
flatpak_debug2 ("Not creating \"%s\" as symlink because its parent g_debug ("Not creating \"%s\" as symlink because its parent is "
is " "already mapped", path);
"already mapped", path);
} }
else else
{ {
g_autofree char *resolved = flatpak_exports_resolve_link_in_host ( exports, g_autofree char *resolved = flatpak_exports_resolve_link_in_host ( exports,
path, path,
NULL); NULL);
if (resolved) if (resolved)
{ {
g_autofree char *parent = g_path_get_dirname (path); g_autofree char *parent = g_path_get_dirname (path);
g_autofree char *relative = make_relative (parent, resolved); g_autofree char *relative = make_relative (parent, resolved);
flatpak_debug2 ("Resolved \"%s\" to \"%s\" in host", path, res g_debug ("Resolved \"%s\" to \"%s\" in host", path, resolved);
olved); g_debug ("Creating \"%s\" -> \"%s\" in sandbox", path, relativ
flatpak_debug2 ("Creating \"%s\" -> \"%s\" in sandbox", path, e);
relative);
flatpak_bwrap_add_args (bwrap, "--symlink", relative, path, N ULL); flatpak_bwrap_add_args (bwrap, "--symlink", relative, path, N ULL);
} }
else else
{ {
flatpak_debug2 ("Unable to resolve \"%s\" in host, skipping", path); g_debug ("Unable to resolve \"%s\" in host, skipping", path);
} }
} }
} }
else if (ep->mode == FAKE_MODE_TMPFS) else if (ep->mode == FAKE_MODE_TMPFS)
{ {
flatpak_debug2 ("\"%s\" is meant to be a tmpfs or empty directory", pa th); g_debug ("\"%s\" is meant to be a tmpfs or empty directory", path);
/* Mount a tmpfs to hide the subdirectory, but only if there /* Mount a tmpfs to hide the subdirectory, but only if there
is a pre-existing dir we can mount the path on. */ is a pre-existing dir we can mount the path on. */
if (path_is_dir (exports, path)) if (path_is_dir (exports, path))
{ {
if (!path_parent_is_mapped (keys, n_keys, exports->hash, path)) if (!path_parent_is_mapped (keys, n_keys, exports->hash, path))
/* If the parent is not mapped, it will be a tmpfs, no need to m ount another one */ /* If the parent is not mapped, it will be a tmpfs, no need to m ount another one */
{ {
flatpak_debug2 ("Parent of \"%s\" is not mapped, creating empt y directory", path); g_debug ("Parent of \"%s\" is not mapped, creating empty direc tory", path);
flatpak_bwrap_add_args (bwrap, "--dir", path, NULL); flatpak_bwrap_add_args (bwrap, "--dir", path, NULL);
} }
else else
{ {
flatpak_debug2 ("Parent of \"%s\" is mapped, creating tmpfs to shadow it", path); g_debug ("Parent of \"%s\" is mapped, creating tmpfs to shadow it", path);
flatpak_bwrap_add_args (bwrap, "--tmpfs", path, NULL); flatpak_bwrap_add_args (bwrap, "--tmpfs", path, NULL);
} }
} }
else else
{ {
flatpak_debug2 ("Not a directory, skipping: \"%s\"", path); g_debug ("Not a directory, skipping: \"%s\"", path);
} }
} }
else if (ep->mode == FAKE_MODE_DIR) else if (ep->mode == FAKE_MODE_DIR)
{ {
flatpak_debug2 ("\"%s\" is meant to be a directory", path); g_debug ("\"%s\" is meant to be a directory", path);
if (path_is_dir (exports, path)) if (path_is_dir (exports, path))
{ {
flatpak_debug2 ("Ensuring \"%s\" is created as a directory", path) ; g_debug ("Ensuring \"%s\" is created as a directory", path);
flatpak_bwrap_add_args (bwrap, "--dir", path, NULL); flatpak_bwrap_add_args (bwrap, "--dir", path, NULL);
} }
else else
{ {
flatpak_debug2 ("Not a directory, skipping: \"%s\"", path); g_debug ("Not a directory, skipping: \"%s\"", path);
} }
} }
else else
{ {
flatpak_debug2 ("\"%s\" is meant to be shared (ro or rw) with the cont g_debug ("\"%s\" is meant to be shared (ro or rw) with the container",
ainer", path);
path);
flatpak_bwrap_add_args (bwrap, flatpak_bwrap_add_args (bwrap,
(ep->mode == FLATPAK_FILESYSTEM_MODE_READ_ONLY ) ? "--ro-bind" : "--bind", (ep->mode == FLATPAK_FILESYSTEM_MODE_READ_ONLY ) ? "--ro-bind" : "--bind",
path, path, NULL); path, path, NULL);
} }
} }
g_assert (exports->host_os >= FLATPAK_FILESYSTEM_MODE_NONE); g_assert (exports->host_os >= FLATPAK_FILESYSTEM_MODE_NONE);
g_assert (exports->host_os <= FLATPAK_FILESYSTEM_MODE_LAST); g_assert (exports->host_os <= FLATPAK_FILESYSTEM_MODE_LAST);
if (exports->host_os != FLATPAK_FILESYSTEM_MODE_NONE) if (exports->host_os != FLATPAK_FILESYSTEM_MODE_NONE)
skipping to change at line 758 skipping to change at line 766
g_return_if_fail (is_export_mode (mode)); g_return_if_fail (is_export_mode (mode));
ep = g_new0 (ExportedPath, 1); ep = g_new0 (ExportedPath, 1);
ep->path = g_strdup (path); ep->path = g_strdup (path);
if (old_ep != NULL) if (old_ep != NULL)
{ {
if (old_ep->mode < mode) if (old_ep->mode < mode)
{ {
flatpak_debug2 ("Increasing export mode from \"%s\" to \"%s\": %s", g_debug ("Increasing export mode from \"%s\" to \"%s\": %s",
export_mode_to_verb (old_ep->mode), export_mode_to_verb (old_ep->mode),
export_mode_to_verb (mode), export_mode_to_verb (mode),
path); path);
ep->mode = mode; ep->mode = mode;
} }
else else
{ {
flatpak_debug2 ("Not changing export mode from \"%s\" to \"%s\": %s", g_debug ("Not changing export mode from \"%s\" to \"%s\": %s",
export_mode_to_verb (old_ep->mode), export_mode_to_verb (old_ep->mode),
export_mode_to_verb (mode), export_mode_to_verb (mode),
path); path);
ep->mode = old_ep->mode; ep->mode = old_ep->mode;
} }
} }
else else
{ {
flatpak_debug2 ("Will %s: %s", export_mode_to_verb (mode), path); g_debug ("Will %s: %s", export_mode_to_verb (mode), path);
ep->mode = mode; ep->mode = mode;
} }
g_hash_table_replace (exports->hash, ep->path, ep); g_hash_table_replace (exports->hash, ep->path, ep);
} }
/* AUTOFS mounts are tricky, as using them as a source in a bind mount /* AUTOFS mounts are tricky, as using them as a source in a bind mount
* causes the mount to trigger, which can take a long time (or forever) * causes the mount to trigger, which can take a long time (or forever)
* waiting for a device or network mount. We try to open the directory * waiting for a device or network mount. We try to open the directory
* but time out after a while, ignoring the mount. Unfortunately we * but time out after a while, ignoring the mount. Unfortunately we
skipping to change at line 864 skipping to change at line 872
if (G_UNLIKELY (exports->test_flags & FLATPAK_EXPORTS_TEST_FLAGS_AUTOFS)) if (G_UNLIKELY (exports->test_flags & FLATPAK_EXPORTS_TEST_FLAGS_AUTOFS))
{ {
if (strcmp (path, "/broken-autofs") == 0) if (strcmp (path, "/broken-autofs") == 0)
return FALSE; return FALSE;
} }
return TRUE; return TRUE;
} }
/* We use level to avoid infinite recursion */ /* We use level to avoid infinite recursion.
*
* Note that some of the errors produced by this function are "real errors"
* and should show up as a user-visible warning, but others are relatively
* uninteresting, and in general none are actually fatal: we prefer to
* continue with fewer paths exposed rather than failing to run. */
static gboolean static gboolean
_exports_path_expose (FlatpakExports *exports, _exports_path_expose (FlatpakExports *exports,
int mode, int mode,
const char *path, const char *path,
int level) int level,
GError **error)
{ {
g_autofree char *canonical = NULL; g_autofree char *canonical = NULL;
struct stat st; struct stat st;
struct statfs stfs; struct statfs stfs;
char *slash; char *slash;
int i; int i;
glnx_autofd int o_path_fd = -1; glnx_autofd int o_path_fd = -1;
g_return_val_if_fail (is_export_mode (mode), FALSE); g_return_val_if_fail (is_export_mode (mode), FALSE);
flatpak_debug2 ("Trying to %s: %s", export_mode_to_verb (mode), path); g_debug ("Trying to %s: %s", export_mode_to_verb (mode), path);
if (level > 40) /* 40 is the current kernel ELOOP check */ if (level > 40) /* 40 is the current kernel ELOOP check */
{ {
g_debug ("Expose too deep, bail"); g_set_error (error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS,
"%s", g_strerror (ELOOP));
return FALSE; return FALSE;
} }
if (!g_path_is_absolute (path)) if (!g_path_is_absolute (path))
{ {
g_debug ("Not exposing relative path %s", path); g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME,
_("An absolute path is required"));
return FALSE; return FALSE;
} }
/* Check if it exists at all */ /* Check if it exists at all */
o_path_fd = flatpak_exports_open_in_host (exports, path, O_PATH | O_NOFOLLOW); o_path_fd = flatpak_exports_open_in_host (exports, path, O_PATH | O_NOFOLLOW);
if (o_path_fd == -1) if (o_path_fd == -1)
{ {
g_debug ("Unable to open path %s to %s: %s", int saved_errno = errno;
path, export_mode_to_verb (mode), g_strerror (errno));
/* Intentionally using G_IO_ERROR_NOT_FOUND even if errno is
* something different, so callers can suppress the warning in this
* relatively likely and uninteresting case: we don't particularly
* care whether this is happening as a result of ENOENT or EACCES
* or any other reason. */
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
_("Unable to open path \"%s\": %s"),
path, g_strerror (saved_errno));
return FALSE; return FALSE;
} }
if (fstat (o_path_fd, &st) != 0) if (fstat (o_path_fd, &st) != 0)
{ return glnx_throw (error,
g_debug ("Unable to get file type of %s: %s", path, g_strerror (errno)); _("Unable to get file type of \"%s\": %s"),
return FALSE; path, g_strerror (errno));
}
/* Don't expose weird things */ /* Don't expose weird things */
if (!(S_ISDIR (st.st_mode) || if (!(S_ISDIR (st.st_mode) ||
S_ISREG (st.st_mode) || S_ISREG (st.st_mode) ||
S_ISLNK (st.st_mode) || S_ISLNK (st.st_mode) ||
S_ISSOCK (st.st_mode))) S_ISSOCK (st.st_mode)))
{ return glnx_throw (error,
g_debug ("%s has unsupported file type 0o%o", path, st.st_mode & S_IFMT); _("File \"%s\" has unsupported type 0o%o"),
return FALSE; path, st.st_mode & S_IFMT);
}
/* O_PATH + fstatfs is the magic that we need to statfs without automounting t he target */ /* O_PATH + fstatfs is the magic that we need to statfs without automounting t he target */
if (fstatfs (o_path_fd, &stfs) != 0) if (fstatfs (o_path_fd, &stfs) != 0)
{ return glnx_throw (error,
g_debug ("Unable to get filesystem information for %s: %s", _("Unable to get filesystem information for \"%s\": %s"),
path, g_strerror (errno)); path, g_strerror (errno));
return FALSE;
}
if (stfs.f_type == AUTOFS_SUPER_MAGIC || if (stfs.f_type == AUTOFS_SUPER_MAGIC ||
(G_UNLIKELY (exports->test_flags & FLATPAK_EXPORTS_TEST_FLAGS_AUTOFS) && (G_UNLIKELY (exports->test_flags & FLATPAK_EXPORTS_TEST_FLAGS_AUTOFS) &&
S_ISDIR (st.st_mode))) S_ISDIR (st.st_mode)))
{ {
if (!check_if_autofs_works (exports, path)) if (!check_if_autofs_works (exports, path))
{ {
g_debug ("ignoring blocking autofs path %s", path); g_set_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK,
_("Ignoring blocking autofs path \"%s\""), path);
return FALSE; return FALSE;
} }
} }
/* Syntactic canonicalization only, no need to use host_fd */ /* Syntactic canonicalization only, no need to use host_fd */
path = canonical = flatpak_canonicalize_filename (path); path = canonical = flatpak_canonicalize_filename (path);
for (i = 0; dont_export_in[i] != NULL; i++) for (i = 0; dont_export_in[i] != NULL; i++)
{ {
/* Don't expose files in non-mounted dirs like /app or /usr, as /* Don't expose files in non-mounted dirs like /app or /usr, as
they are not the same as on the host, and we generally can't they are not the same as on the host, and we generally can't
create the parents for them anyway */ create the parents for them anyway */
if (flatpak_has_path_prefix (path, dont_export_in[i])) if (flatpak_has_path_prefix (path, dont_export_in[i]))
{ {
g_debug ("skipping export for path %s in unsupported prefix", path); g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTABLE_FILE,
_("Path \"%s\" is reserved by Flatpak"),
dont_export_in[i]);
return FALSE;
}
/* Also don't expose directories that are a parent of a directory
* that is "owned" by the sandboxing framework. For example, because
* Flatpak controls /run/host and /run/flatpak, we cannot allow
* --filesystem=/run, which would prevent us from creating the
* contents of /run/host and /run/flatpak. */
if (flatpak_has_path_prefix (dont_export_in[i], path))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTABLE_FILE,
_("Path \"%s\" is reserved by Flatpak"),
dont_export_in[i]);
return FALSE; return FALSE;
} }
} }
for (i = 0; flatpak_abs_usrmerged_dirs[i] != NULL; i++) for (i = 0; flatpak_abs_usrmerged_dirs[i] != NULL; i++)
{ {
/* Same as /usr, but for the directories that get merged into /usr */ /* Same as /usr, but for the directories that get merged into /usr.
* Keep the translatable string here the same as the one above */
if (flatpak_has_path_prefix (path, flatpak_abs_usrmerged_dirs[i])) if (flatpak_has_path_prefix (path, flatpak_abs_usrmerged_dirs[i]))
{ {
g_debug ("skipping export for path %s in a /usr-merged directory", pat g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTABLE_FILE,
h); _("Path \"%s\" is reserved by Flatpak"),
flatpak_abs_usrmerged_dirs[i]);
return FALSE; return FALSE;
} }
} }
/* Handle any symlinks prior to the target itself. This includes path itself, /* Handle any symlinks prior to the target itself. This includes path itself,
because we expose the target of the symlink. */ because we expose the target of the symlink. */
slash = canonical; slash = canonical;
do do
{ {
slash = strchr (slash + 1, '/'); slash = strchr (slash + 1, '/');
if (slash) if (slash)
*slash = 0; *slash = 0;
if (!path_is_symlink (exports, path)) if (!path_is_symlink (exports, path))
{ {
flatpak_debug2 ("%s is not a symlink", path); g_debug ("%s is not a symlink", path);
} }
else if (never_export_as_symlink (path)) else if (never_export_as_symlink (path))
{ {
flatpak_debug2 ("%s is a symlink, but we avoid exporting it as such", path); g_debug ("%s is a symlink, but we avoid exporting it as such", path);
} }
else else
{ {
g_autoptr(GError) error = NULL; g_autoptr(GError) local_error = NULL;
g_autofree char *resolved = flatpak_exports_resolve_link_in_host (expo g_autofree char *resolved = flatpak_exports_resolve_link_in_host (expo
rts, path, &error); rts, path, &local_error);
g_autofree char *new_target = NULL; g_autofree char *new_target = NULL;
if (resolved) if (resolved)
{ {
flatpak_debug2 ("%s is a symlink, resolved to %s", path, resolved) ; g_debug ("%s is a symlink, resolved to %s", path, resolved);
if (slash) if (slash)
new_target = g_build_filename (resolved, slash + 1, NULL); new_target = g_build_filename (resolved, slash + 1, NULL);
else else
new_target = g_strdup (resolved); new_target = g_strdup (resolved);
flatpak_debug2 ("Trying to export the target instead: %s", new_tar get); g_debug ("Trying to export the target instead: %s", new_target);
if (_exports_path_expose (exports, mode, new_target, level + 1)) if (_exports_path_expose (exports, mode, new_target, level + 1, &l ocal_error))
{ {
do_export_path (exports, path, FAKE_MODE_SYMLINK); do_export_path (exports, path, FAKE_MODE_SYMLINK);
return TRUE; return TRUE;
} }
flatpak_debug2 ("Could not export target %s, so ignoring %s", g_debug ("Could not export target %s, so ignoring %s",
new_target, path); new_target, path);
g_propagate_error (error, g_steal_pointer (&local_error));
return FALSE; return FALSE;
} }
else else
{ {
flatpak_debug2 ("%s is a symlink but we were unable to resolve it: g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
%s", _("Unable to resolve symbolic link \"%s\": %s"),
path, error->message); path, local_error->message);
return FALSE; return FALSE;
} }
} }
if (slash) if (slash)
*slash = '/'; *slash = '/';
} }
while (slash != NULL); while (slash != NULL);
do_export_path (exports, path, mode); do_export_path (exports, path, mode);
return TRUE; return TRUE;
} }
void gboolean
flatpak_exports_add_path_expose (FlatpakExports *exports, flatpak_exports_add_path_expose (FlatpakExports *exports,
FlatpakFilesystemMode mode, FlatpakFilesystemMode mode,
const char *path) const char *path,
{ GError **error)
g_return_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE); {
g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); g_return_val_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE, FALSE);
_exports_path_expose (exports, mode, path, 0); g_return_val_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST, FALSE);
return _exports_path_expose (exports, mode, path, 0, error);
} }
void gboolean
flatpak_exports_add_path_tmpfs (FlatpakExports *exports, flatpak_exports_add_path_tmpfs (FlatpakExports *exports,
const char *path) const char *path,
GError **error)
{ {
_exports_path_expose (exports, FAKE_MODE_TMPFS, path, 0); return _exports_path_expose (exports, FAKE_MODE_TMPFS, path, 0, error);
} }
void gboolean
flatpak_exports_add_path_expose_or_hide (FlatpakExports *exports, flatpak_exports_add_path_expose_or_hide (FlatpakExports *exports,
FlatpakFilesystemMode mode, FlatpakFilesystemMode mode,
const char *path) const char *path,
GError **error)
{ {
g_return_if_fail (mode >= FLATPAK_FILESYSTEM_MODE_NONE); g_return_val_if_fail (mode >= FLATPAK_FILESYSTEM_MODE_NONE, FALSE);
g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); g_return_val_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST, FALSE);
if (mode == FLATPAK_FILESYSTEM_MODE_NONE) if (mode == FLATPAK_FILESYSTEM_MODE_NONE)
flatpak_exports_add_path_tmpfs (exports, path); return flatpak_exports_add_path_tmpfs (exports, path, error);
else else
flatpak_exports_add_path_expose (exports, mode, path); return flatpak_exports_add_path_expose (exports, mode, path, error);
} }
void gboolean
flatpak_exports_add_path_dir (FlatpakExports *exports, flatpak_exports_add_path_dir (FlatpakExports *exports,
const char *path) const char *path,
GError **error)
{ {
_exports_path_expose (exports, FAKE_MODE_DIR, path, 0); return _exports_path_expose (exports, FAKE_MODE_DIR, path, 0, error);
} }
void void
flatpak_exports_add_host_etc_expose (FlatpakExports *exports, flatpak_exports_add_host_etc_expose (FlatpakExports *exports,
FlatpakFilesystemMode mode) FlatpakFilesystemMode mode)
{ {
g_return_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE); g_return_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE);
g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST);
exports->host_etc = mode; exports->host_etc = mode;
 End of changes. 47 change blocks. 
92 lines changed or deleted 132 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)