From 11af00194e3e0ec15e17a23556dc2929f92e0210 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 1 Jan 2026 17:01:03 +0100 Subject: Bumped copyright year. --- snac.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'snac.c') diff --git a/snac.c b/snac.c index 31f524f..965edbb 100644 --- a/snac.c +++ b/snac.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #define XS_IMPLEMENTATION -- cgit From 688c54c87355b5424f33f7b089814460a74af594 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Tue, 6 Jan 2026 11:02:36 +0100 Subject: Implement configurable EXIF stripping for uploaded media - Add `strip_exif` configuration option to enable metadata removal. - Add `mogrify_path` configuration to specify external tool location. - Implement strip_media using `mogrify -strip`. - Support multiple image formats: jpg, png, webp, heic, heif, avif, tiff, gif, bmp. - Add strict startup check: fail to start if `strip_exif` is enabled but `mogrify` is missing/broken. - Update documentation in `doc/snac.8`. --- data.c | 13 +++++++++++-- doc/snac.8 | 8 ++++++++ snac.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ snac.h | 3 +++ 4 files changed, 86 insertions(+), 2 deletions(-) (limited to 'snac.c') 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..8283ac6 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -296,6 +296,14 @@ 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 metadata will be stripped from uploaded images (jpg, png, webp, heic, avif, tiff, gif, bmp). This requires the +.Nm mogrify +tool to be installed. If +.Nm snac +cannot find or execute the tool 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. .El .Pp You must restart the server to make effective these changes. diff --git a/snac.c b/snac.c index 965edbb..f4528cd 100644 --- a/snac.c +++ b/snac.c @@ -32,6 +32,7 @@ #include #include +#include xs_str *srv_basedir = NULL; xs_dict *srv_config = NULL; @@ -170,3 +171,66 @@ 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) { + xs *l_fn = xs_tolower_i(xs_dup(fn)); + + /* check 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("%s -strip \"%s\" 2>/dev/null", mp, 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.", fn, mp)); + else + srv_log(xs_fmt("strip_media: error stripping %s %d", fn, ret)); + } + else + srv_debug(1, xs_fmt("strip_media: stripped %s", 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) { + 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) + ret = 0; + } + + return ret; +} diff --git a/snac.h b/snac.h index 8a7dad6..c307dbd 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, -- cgit From 03d270a56b751bc53b83381d9fef21da8d4cbb91 Mon Sep 17 00:00:00 2001 From: Stefano Marinelli Date: Tue, 6 Jan 2026 12:18:00 +0100 Subject: Implement metadata stripping for uploaded videos - Extend `strip_media` to support video files using `ffmpeg`. - Use `ffmpeg -map_metadata -1 -c copy` to strip global metadata. - Support formats: mp4, m4v, mov, webm, mkv, avi. - Add `ffmpeg_path` configuration option. - Implement robust relative path handling (`user/` heuristic) to support jailed environments. - Enforce strict checks on startup: fail if tools (mogrify/ffmpeg) are missing when enabled. --- doc/snac.8 | 10 ++++-- snac.c | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 106 insertions(+), 16 deletions(-) (limited to 'snac.c') diff --git a/doc/snac.8 b/doc/snac.8 index 8283ac6..c53bb59 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -297,13 +297,17 @@ 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 metadata will be stripped from uploaded images (jpg, png, webp, heic, avif, tiff, gif, bmp). This requires the +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 -tool to be installed. If +(from ImageMagick) and +.Nm ffmpeg +tools to be installed. If .Nm snac -cannot find or execute the tool at startup, it will refuse to run. +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 f4528cd..a3ba6b7 100644 --- a/snac.c +++ b/snac.c @@ -33,6 +33,8 @@ #include #include #include +#include +#include xs_str *srv_basedir = NULL; xs_dict *srv_config = NULL; @@ -177,12 +179,25 @@ 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) { - xs *l_fn = xs_tolower_i(xs_dup(fn)); + /* 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; + } - /* check extensions */ + 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") || @@ -193,7 +208,7 @@ int strip_media(const char *fn) if (mp == NULL) mp = "mogrify"; - xs *cmd = xs_fmt("%s -strip \"%s\" 2>/dev/null", mp, fn); + xs *cmd = xs_fmt("cd \"%s\" && %s -auto-orient -strip \"%s\" 2>/dev/null", srv_basedir, mp, r_fn); ret = system(cmd); @@ -203,12 +218,64 @@ int strip_media(const char *fn) 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.", fn, mp)); + 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", fn, ret)); + srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret)); } else - srv_debug(1, xs_fmt("strip_media: stripped %s", fn)); + 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)); + } } } @@ -222,14 +289,33 @@ int check_strip_tool(void) int ret = 1; if (xs_type(v) == XSTYPE_TRUE) { - const char *mp = xs_dict_get(srv_config, "mogrify_path"); - if (mp == NULL) - mp = "mogrify"; + /* 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) - ret = 0; + 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; -- cgit From fdd2217be47799d93fe7505faffd7c948d69ecdb Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 10 Jan 2026 20:40:13 +0100 Subject: New file xs_list_tools.h. --- snac.c | 1 + xs_list_tools.h | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xs_version.h | 2 +- 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 xs_list_tools.h (limited to 'snac.c') diff --git a/snac.c b/snac.c index 965edbb..41db86d 100644 --- a/snac.c +++ b/snac.c @@ -27,6 +27,7 @@ #include "xs_html.h" #include "xs_po.h" #include "xs_webmention.h" +#include "xs_list_tools.h" #include "snac.h" diff --git a/xs_list_tools.h b/xs_list_tools.h new file mode 100644 index 0000000..33d4b87 --- /dev/null +++ b/xs_list_tools.h @@ -0,0 +1,169 @@ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ + +#ifndef _XS_LIST_TOOLS_H + +#define _XS_LIST_TOOLS_H + + xs_list *xs_list_insert_sorted(xs_list *list, const xs_val *nv); + xs_list *xs_list_reverse(const xs_list *l); + xs_val **xs_list_to_array(const xs_list *l, int *len); + int xs_list_sort_cmp(const void *p1, const void *p2); + int xs_list_sort_inv_cmp(const void *p1, const void *p2); + int xs_list_sort_dict_cmp(const char *field, const void *p1, const void *p2); + xs_list *xs_list_sort(const xs_list *l, int (*cmp)(const void *, const void *)); + xs_list *xs_list_shuffle(const xs_list *l); + +#ifdef XS_IMPLEMENTATION + +#include "xs_random.h" + +xs_list *xs_list_insert_sorted(xs_list *list, const xs_val *nv) +/* inserts a string in the list in its ordered position */ +{ + XS_ASSERT_TYPE(list, XSTYPE_LIST); + + int offset = xs_size(list); + + const xs_val *v; + xs_list_foreach(list, v) { + /* if this element is greater or equal, insert here */ + if (xs_cmp(v, nv) >= 0) { + offset = v - list; + break; + } + } + + return _xs_list_write_litem(list, offset - 1, nv, xs_size(nv)); +} + + +xs_list *xs_list_reverse(const xs_list *l) +/* creates a new list as a reverse version of l */ +{ + xs_list *n = xs_dup(l); + const xs_val *v; + + /* move to one byte before the EOM */ + char *p = n + xs_size(n) - 1; + + xs_list_foreach(l, v) { + /* size of v, plus the LITEM */ + int z = xs_size(v) + 1; + + p -= z; + + /* copy v, including its LITEM */ + memcpy(p, v - 1, z); + } + + return n; +} + + +xs_val **xs_list_to_array(const xs_list *l, int *len) +/* converts a list to an array of values */ +/* must be freed after use */ +{ + *len = xs_list_len(l); + xs_val **a = xs_realloc(NULL, *len * sizeof(xs_val *)); + const xs_val *v; + int n = 0; + + xs_list_foreach(l, v) + a[n++] = (xs_val *)v; + + return a; +} + + +int xs_list_sort_cmp(const void *p1, const void *p2) +/* default list sorting function */ +{ + const xs_val *v1 = *(xs_val **)p1; + const xs_val *v2 = *(xs_val **)p2; + + return xs_cmp(v1, v2); +} + + +int xs_list_sort_inv_cmp(const void *p1, const void *p2) +/* default list inverse sorting function */ +{ + const xs_val *v1 = *(xs_val **)p1; + const xs_val *v2 = *(xs_val **)p2; + + return xs_cmp(v2, v1); +} + + +int xs_list_sort_dict_cmp(const char *field, const void *p1, const void *p2) +/* compare sorting function for a field an array of dicts */ +{ + const xs_dict *d1 = *(xs_val **)p1; + const xs_dict *d2 = *(xs_val **)p2; + + if (xs_type(d1) != XSTYPE_DICT || xs_type(d2) != XSTYPE_DICT) + return 0; + + return xs_cmp(xs_dict_get_def(d1, field, ""), + xs_dict_get_def(d2, field, "")); +} + + +xs_list *xs_list_sort(const xs_list *l, int (*cmp)(const void *, const void *)) +/* returns a sorted copy of l. cmp can be null for standard sorting */ +{ + int sz; + xs_val **a = xs_list_to_array(l, &sz); + xs_list *nl = xs_dup(l); + char *p = nl + 1 + _XS_TYPE_SIZE; + + /* sort the array */ + qsort(a, sz, sizeof(xs_val *), cmp ? cmp : xs_list_sort_cmp); + + /* transfer the sorted list over the copy */ + for (int n = 0; n < sz; n++) { + /* get the litem */ + const char *e = a[n] - 1; + int z = xs_size(e); + + memcpy(p, e, z); + p += z; + } + + xs_free(a); + + return nl; +} + + +xs_list *xs_list_shuffle(const xs_list *l) +/* returns a shuffled list */ +{ + int sz; + xs_val **a = xs_list_to_array(l, &sz); + xs_list *nl = xs_list_new(); + unsigned int seed = 0; + + xs_rnd_buf(&seed, sizeof(seed)); + + /* shuffle */ + for (int n = sz - 1; n > 0; n--) { + int m = xs_rnd_int32_d(&seed) % n; + void *p = a[n]; + a[n] = a[m]; + a[m] = p; + } + + for (int n = 0; n < sz; n++) + nl = xs_list_append(nl, a[n]); + + xs_free(a); + + return nl; +} + + +#endif /* XS_IMPLEMENTATION */ + +#endif /* XS_LIST_TOOLS_H */ diff --git a/xs_version.h b/xs_version.h index 598c72e..92a865e 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* ad74258be9b1585840a5366cdb4b6ef707c0e95a 2026-01-01T16:58:39+01:00 */ +/* 270f9376eabd4f8e0ed3ae22a1f8eb6e06ea8b8b 2026-01-10T20:39:12+01:00 */ -- cgit