summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Stefano Marinelli2026-01-06 12:18:00 +0100
committerGravatar Stefano Marinelli2026-01-06 12:18:00 +0100
commit03d270a56b751bc53b83381d9fef21da8d4cbb91 (patch)
tree420ee265ed31995479e8660d33cb60c89472a6b0
parentImplement configurable EXIF stripping for uploaded media (diff)
downloadsnac2-03d270a56b751bc53b83381d9fef21da8d4cbb91.tar.gz
snac2-03d270a56b751bc53b83381d9fef21da8d4cbb91.tar.xz
snac2-03d270a56b751bc53b83381d9fef21da8d4cbb91.zip
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.
-rw-r--r--doc/snac.810
-rw-r--r--snac.c112
2 files changed, 106 insertions, 16 deletions
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.
297.It Ic vkey 297.It Ic vkey
298Public vapid key. Used for notification on some client. 298Public vapid key. Used for notification on some client.
299.It Ic strip_exif 299.It Ic strip_exif
300If set to true, EXIF metadata will be stripped from uploaded images (jpg, png, webp, heic, avif, tiff, gif, bmp). This requires the 300If 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
301.Nm mogrify 301.Nm mogrify
302tool to be installed. If 302(from ImageMagick) and
303.Nm ffmpeg
304tools to be installed. If
303.Nm snac 305.Nm snac
304cannot find or execute the tool at startup, it will refuse to run. 306cannot find or execute these tools at startup, it will refuse to run.
305.It Ic mogrify_path 307.It Ic mogrify_path
306Overrides the default "mogrify" command name or path. Use this if the tool is not in the system PATH or has a different name. 308Overrides the default "mogrify" command name or path. Use this if the tool is not in the system PATH or has a different name.
309.It Ic ffmpeg_path
310Overrides the default "ffmpeg" command name or path. Use this if the tool is not in the system PATH or has a different name.
307.El 311.El
308.Pp 312.Pp
309You must restart the server to make effective these changes. 313You 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 @@
33#include <sys/time.h> 33#include <sys/time.h>
34#include <sys/stat.h> 34#include <sys/stat.h>
35#include <sys/wait.h> 35#include <sys/wait.h>
36#include <limits.h>
37#include <stdlib.h>
36 38
37xs_str *srv_basedir = NULL; 39xs_str *srv_basedir = NULL;
38xs_dict *srv_config = NULL; 40xs_dict *srv_config = NULL;
@@ -177,12 +179,25 @@ int strip_media(const char *fn)
177/* strips EXIF data from a file */ 179/* strips EXIF data from a file */
178{ 180{
179 int ret = 0; 181 int ret = 0;
182
180 const xs_val *v = xs_dict_get(srv_config, "strip_exif"); 183 const xs_val *v = xs_dict_get(srv_config, "strip_exif");
181 184
182 if (xs_type(v) == XSTYPE_TRUE) { 185 if (xs_type(v) == XSTYPE_TRUE) {
183 xs *l_fn = xs_tolower_i(xs_dup(fn)); 186 /* Heuristic: find 'user/' in the path to make it relative */
187 /* This works for ~/user/..., /var/snac/user/..., etc. */
188 const char *r_fn = strstr(fn, "user/");
189
190 if (r_fn == NULL) {
191 /* Fallback: try to strip ~/ if present */
192 if (strncmp(fn, "~/", 2) == 0)
193 r_fn = fn + 2;
194 else
195 r_fn = fn;
196 }
184 197
185 /* check extensions */ 198 xs *l_fn = xs_tolower_i(xs_dup(r_fn));
199
200 /* check image extensions */
186 if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") || 201 if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") ||
187 xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") || 202 xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") ||
188 xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") || 203 xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") ||
@@ -193,7 +208,7 @@ int strip_media(const char *fn)
193 if (mp == NULL) 208 if (mp == NULL)
194 mp = "mogrify"; 209 mp = "mogrify";
195 210
196 xs *cmd = xs_fmt("%s -strip \"%s\" 2>/dev/null", mp, fn); 211 xs *cmd = xs_fmt("cd \"%s\" && %s -auto-orient -strip \"%s\" 2>/dev/null", srv_basedir, mp, r_fn);
197 212
198 ret = system(cmd); 213 ret = system(cmd);
199 214
@@ -203,12 +218,64 @@ int strip_media(const char *fn)
203 code = WEXITSTATUS(ret); 218 code = WEXITSTATUS(ret);
204 219
205 if (code == 127) 220 if (code == 127)
206 srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", fn, mp)); 221 srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'mogrify_path' in server.json.", r_fn, mp));
207 else 222 else
208 srv_log(xs_fmt("strip_media: error stripping %s %d", fn, ret)); 223 srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
209 } 224 }
210 else 225 else
211 srv_debug(1, xs_fmt("strip_media: stripped %s", fn)); 226 srv_debug(1, xs_fmt("strip_media: stripped %s", r_fn));
227 }
228 else
229 /* check video extensions */
230 if (xs_endswith(l_fn, ".mp4") || xs_endswith(l_fn, ".m4v") ||
231 xs_endswith(l_fn, ".mov") || xs_endswith(l_fn, ".webm") ||
232 xs_endswith(l_fn, ".mkv") || xs_endswith(l_fn, ".avi")) {
233
234 const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
235 if (fp == NULL)
236 fp = "ffmpeg";
237
238 /* ffmpeg cannot modify in-place, so we need a temp file */
239 /* we must preserve valid extension for ffmpeg to guess the format */
240 const char *ext = strrchr(r_fn, '.');
241 if (ext == NULL) ext = "";
242 xs *tmp_fn = xs_fmt("%s.tmp%s", r_fn, ext);
243
244 /* -map_metadata -1 strips all global metadata */
245 /* -c copy copies input streams without re-encoding */
246 /* we don't silence stderr so we can debug issues */
247 /* we explicitly cd to srv_basedir to ensure relative paths work */
248 xs *cmd = xs_fmt("cd \"%s\" && %s -y -i \"%s\" -map_metadata -1 -c copy \"%s\"", srv_basedir, fp, r_fn, tmp_fn);
249
250 ret = system(cmd);
251
252 if (ret != 0) {
253 int code = 0;
254 if (WIFEXITED(ret))
255 code = WEXITSTATUS(ret);
256
257 if (code == 127)
258 srv_log(xs_fmt("strip_media: error stripping %s. '%s' not found (exit 127). Set 'ffmpeg_path' in server.json.", r_fn, fp));
259 else {
260 srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
261 srv_log(xs_fmt("strip_media: command was: %s", cmd));
262 }
263
264 /* try to cleanup, just in case */
265 /* unlink needs full path too if we are not in basedir */
266 xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
267 unlink(full_tmp_fn);
268 }
269 else {
270 /* rename tmp file to original */
271 /* use full path for source because it was created relative to basedir */
272 xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
273
274 if (rename(full_tmp_fn, fn) == 0)
275 srv_debug(1, xs_fmt("strip_media: stripped %s", fn));
276 else
277 srv_log(xs_fmt("strip_media: error renaming %s to %s", full_tmp_fn, fn));
278 }
212 } 279 }
213 } 280 }
214 281
@@ -222,14 +289,33 @@ int check_strip_tool(void)
222 int ret = 1; 289 int ret = 1;
223 290
224 if (xs_type(v) == XSTYPE_TRUE) { 291 if (xs_type(v) == XSTYPE_TRUE) {
225 const char *mp = xs_dict_get(srv_config, "mogrify_path"); 292 /* check mogrify */
226 if (mp == NULL) 293 {
227 mp = "mogrify"; 294 const char *mp = xs_dict_get(srv_config, "mogrify_path");
295 if (mp == NULL)
296 mp = "mogrify";
228 297
229 xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp); 298 xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", mp);
230 299
231 if (system(cmd) != 0) 300 if (system(cmd) != 0) {
232 ret = 0; 301 srv_log(xs_fmt("check_strip_tool: '%s' not working", mp));
302 ret = 0;
303 }
304 }
305
306 /* check ffmpeg */
307 if (ret) {
308 const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
309 if (fp == NULL)
310 fp = "ffmpeg";
311
312 xs *cmd = xs_fmt("%s -version 2>/dev/null >/dev/null", fp);
313
314 if (system(cmd) != 0) {
315 srv_log(xs_fmt("check_strip_tool: '%s' not working", fp));
316 ret = 0;
317 }
318 }
233 } 319 }
234 320
235 return ret; 321 return ret;