aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrunfink <grunfink@noreply.codeberg.org>2026-01-12 14:27:05 +0100
committergrunfink <grunfink@noreply.codeberg.org>2026-01-12 14:27:05 +0100
commit3ffdf91a4c6e90b0738a767d75b80941500713e0 (patch)
treec716b2fc1121c97a4ee82f3993d97ed41e63a7ad
parent9a5f67bd30511291f7ac9a13a56fb5611b4350cf (diff)
parent03d270a56b751bc53b83381d9fef21da8d4cbb91 (diff)
Merge pull request 'Implement metadata stripping for uploaded photos and videos' (#515) from draga79/snac2:master into master
Reviewed-on: https://codeberg.org/grunfink/snac2/pulls/515
-rw-r--r--data.c13
-rw-r--r--doc/snac.812
-rw-r--r--snac.c150
-rw-r--r--snac.h3
4 files changed, 176 insertions, 2 deletions
diff --git a/data.c b/data.c
index 22ea7b0..f32dc81 100644
--- a/data.c
+++ b/data.c
@@ -89,8 +89,15 @@ int srv_open(const char *basedir, int auto_upgrade)
else {
if (xs_number_get(xs_dict_get(srv_config, "layout")) < disk_layout)
error = xs_fmt("ERROR: disk layout changed - execute 'snac upgrade' first");
- else
- ret = 1;
+ else {
+ if (!check_strip_tool()) {
+ const char *mp = xs_dict_get(srv_config, "mogrify_path");
+ if (mp == NULL) mp = "mogrify";
+ error = xs_fmt("ERROR: strip_exif enabled but '%s' not found or not working (set 'mogrify_path' in server.json)", mp);
+ }
+ else
+ ret = 1;
+ }
}
}
@@ -2710,6 +2717,8 @@ void static_put(snac *snac, const char *id, const char *data, int size)
if (fn && (f = fopen(fn, "wb")) != NULL) {
fwrite(data, size, 1, f);
fclose(f);
+
+ strip_media(fn);
}
}
diff --git a/doc/snac.8 b/doc/snac.8
index b8a75fa..c53bb59 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -296,6 +296,18 @@ outgoing messages (default: 15). Anyway, whenever any incoming activity from a
failed instance is detected, this counter is reset for it.
.It Ic vkey
Public vapid key. Used for notification on some client.
+.It Ic strip_exif
+If set to true, EXIF and other metadata will be stripped from uploaded images (jpg, png, webp, heic, avif, tiff, gif, bmp) and videos (mp4, m4v, mov, webm, mkv, avi). This requires the
+.Nm mogrify
+(from ImageMagick) and
+.Nm ffmpeg
+tools to be installed. If
+.Nm snac
+cannot find or execute these tools at startup, it will refuse to run.
+.It Ic mogrify_path
+Overrides the default "mogrify" command name or path. Use this if the tool is not in the system PATH or has a different name.
+.It Ic ffmpeg_path
+Overrides the default "ffmpeg" command name or path. Use this if the tool is not in the system PATH or has a different name.
.El
.Pp
You must restart the server to make effective these changes.
diff --git a/snac.c b/snac.c
index 41db86d..87b0d63 100644
--- a/snac.c
+++ b/snac.c
@@ -33,6 +33,9 @@
#include <sys/time.h>
#include <sys/stat.h>
+#include <sys/wait.h>
+#include <limits.h>
+#include <stdlib.h>
xs_str *srv_basedir = NULL;
xs_dict *srv_config = NULL;
@@ -171,3 +174,150 @@ int check_password(const char *uid, const char *passwd, const char *hash)
return ret;
}
+
+
+int strip_media(const char *fn)
+/* strips EXIF data from a file */
+{
+ int ret = 0;
+
+ const xs_val *v = xs_dict_get(srv_config, "strip_exif");
+
+ if (xs_type(v) == XSTYPE_TRUE) {
+ /* Heuristic: find 'user/' in the path to make it relative */
+ /* This works for ~/user/..., /var/snac/user/..., etc. */
+ const char *r_fn = strstr(fn, "user/");
+
+ if (r_fn == NULL) {
+ /* Fallback: try to strip ~/ if present */
+ if (strncmp(fn, "~/", 2) == 0)
+ r_fn = fn + 2;
+ else
+ r_fn = fn;
+ }
+
+ xs *l_fn = xs_tolower_i(xs_dup(r_fn));
+
+ /* check image extensions */
+ if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") ||
+ xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") ||
+ xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") ||
+ xs_endswith(l_fn, ".avif") || xs_endswith(l_fn, ".tiff") ||
+ xs_endswith(l_fn, ".gif") || xs_endswith(l_fn, ".bmp")) {
+
+ const char *mp = xs_dict_get(srv_config, "mogrify_path");
+ if (mp == NULL)
+ mp = "mogrify";
+
+ xs *cmd = xs_fmt("cd \"%s\" && %s -auto-orient -strip \"%s\" 2>/dev/null", srv_basedir, mp, r_fn);
+
+ ret = system(cmd);
+
+ if (ret != 0) {
+ int code = 0;
+ if (WIFEXITED(ret))
+ code = WEXITSTATUS(ret);
+
+ if (code == 127)
+ srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", r_fn, mp));
+ else
+ srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
+ }
+ else
+ srv_debug(1, xs_fmt("strip_media: stripped %s", r_fn));
+ }
+ else
+ /* check video extensions */
+ if (xs_endswith(l_fn, ".mp4") || xs_endswith(l_fn, ".m4v") ||
+ xs_endswith(l_fn, ".mov") || xs_endswith(l_fn, ".webm") ||
+ xs_endswith(l_fn, ".mkv") || xs_endswith(l_fn, ".avi")) {
+
+ const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
+ if (fp == NULL)
+ fp = "ffmpeg";
+
+ /* ffmpeg cannot modify in-place, so we need a temp file */
+ /* we must preserve valid extension for ffmpeg to guess the format */
+ const char *ext = strrchr(r_fn, '.');
+ if (ext == NULL) ext = "";
+ xs *tmp_fn = xs_fmt("%s.tmp%s", r_fn, ext);
+
+ /* -map_metadata -1 strips all global metadata */
+ /* -c copy copies input streams without re-encoding */
+ /* we don't silence stderr so we can debug issues */
+ /* we explicitly cd to srv_basedir to ensure relative paths work */
+ xs *cmd = xs_fmt("cd \"%s\" && %s -y -i \"%s\" -map_metadata -1 -c copy \"%s\"", srv_basedir, fp, r_fn, tmp_fn);
+
+ ret = system(cmd);
+
+ if (ret != 0) {
+ int code = 0;
+ if (WIFEXITED(ret))
+ code = WEXITSTATUS(ret);
+
+ if (code == 127)
+ srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'ffmpeg_path' in server.json.", r_fn, fp));
+ else {
+ srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
+ srv_log(xs_fmt("strip_media: command was: %s", cmd));
+ }
+
+ /* try to cleanup, just in case */
+ /* unlink needs full path too if we are not in basedir */
+ xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
+ unlink(full_tmp_fn);
+ }
+ else {
+ /* rename tmp file to original */
+ /* use full path for source because it was created relative to basedir */
+ xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
+
+ if (rename(full_tmp_fn, fn) == 0)
+ srv_debug(1, xs_fmt("strip_media: stripped %s", fn));
+ else
+ srv_log(xs_fmt("strip_media: error renaming %s to %s", full_tmp_fn, fn));
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+int check_strip_tool(void)
+{
+ const xs_val *v = xs_dict_get(srv_config, "strip_exif");
+ int ret = 1;
+
+ if (xs_type(v) == XSTYPE_TRUE) {
+ /* check mogrify */
+ {
+ const char *mp = xs_dict_get(srv_config, "mogrify_path");
+ if (mp == NULL)
+ mp = "mogrify";
+
+ xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp);
+
+ if (system(cmd) != 0) {
+ srv_log(xs_fmt("check_strip_tool: '%s' not working", mp));
+ ret = 0;
+ }
+ }
+
+ /* check ffmpeg */
+ if (ret) {
+ const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
+ if (fp == NULL)
+ fp = "ffmpeg";
+
+ xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", fp);
+
+ if (system(cmd) != 0) {
+ srv_log(xs_fmt("check_strip_tool: '%s' not working", fp));
+ ret = 0;
+ }
+ }
+ }
+
+ return ret;
+}
diff --git a/snac.h b/snac.h
index d57391f..469982d 100644
--- a/snac.h
+++ b/snac.h
@@ -105,6 +105,9 @@ int validate_uid(const char *uid);
xs_str *hash_password(const char *uid, const char *passwd, const char *nonce);
int check_password(const char *uid, const char *passwd, const char *hash);
+int strip_media(const char *fn);
+int check_strip_tool(void);
+
void srv_archive(const char *direction, const char *url, xs_dict *req,
const char *payload, int p_size,
int status, xs_dict *headers,