diff options
| author | 2026-01-06 12:18:00 +0100 | |
|---|---|---|
| committer | 2026-01-06 12:18:00 +0100 | |
| commit | 03d270a56b751bc53b83381d9fef21da8d4cbb91 (patch) | |
| tree | 420ee265ed31995479e8660d33cb60c89472a6b0 | |
| parent | Implement configurable EXIF stripping for uploaded media (diff) | |
| download | snac2-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.8 | 10 | ||||
| -rw-r--r-- | snac.c | 112 |
2 files changed, 106 insertions, 16 deletions
| @@ -297,13 +297,17 @@ failed instance is detected, this counter is reset for it. | |||
| 297 | .It Ic vkey | 297 | .It Ic vkey |
| 298 | Public vapid key. Used for notification on some client. | 298 | Public vapid key. Used for notification on some client. |
| 299 | .It Ic strip_exif | 299 | .It Ic strip_exif |
| 300 | If set to true, EXIF metadata will be stripped from uploaded images (jpg, png, webp, heic, avif, tiff, gif, bmp). This requires the | 300 | 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 |
| 301 | .Nm mogrify | 301 | .Nm mogrify |
| 302 | tool to be installed. If | 302 | (from ImageMagick) and |
| 303 | .Nm ffmpeg | ||
| 304 | tools to be installed. If | ||
| 303 | .Nm snac | 305 | .Nm snac |
| 304 | cannot find or execute the tool at startup, it will refuse to run. | 306 | cannot find or execute these tools at startup, it will refuse to run. |
| 305 | .It Ic mogrify_path | 307 | .It Ic mogrify_path |
| 306 | Overrides the default "mogrify" command name or path. Use this if the tool is not in the system PATH or has a different name. | 308 | Overrides 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 | ||
| 310 | Overrides 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 |
| 309 | You must restart the server to make effective these changes. | 313 | You must restart the server to make effective these changes. |
| @@ -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 | ||
| 37 | xs_str *srv_basedir = NULL; | 39 | xs_str *srv_basedir = NULL; |
| 38 | xs_dict *srv_config = NULL; | 40 | xs_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; |