snac.c 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. /* snac - A simple, minimalistic ActivityPub instance */
  2. /* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
  3. #define XS_IMPLEMENTATION
  4. #include "xs.h"
  5. #include "xs_hex.h"
  6. #include "xs_io.h"
  7. #include "xs_unicode_tbl.h"
  8. #include "xs_unicode.h"
  9. #include "xs_json.h"
  10. #include "xs_curl.h"
  11. #include "xs_openssl.h"
  12. #include "xs_socket.h"
  13. #include "xs_unix_socket.h"
  14. #include "xs_url.h"
  15. #include "xs_http.h"
  16. #include "xs_httpd.h"
  17. #include "xs_mime.h"
  18. #include "xs_regex.h"
  19. #include "xs_set.h"
  20. #include "xs_time.h"
  21. #include "xs_glob.h"
  22. #include "xs_random.h"
  23. #include "xs_match.h"
  24. #include "xs_fcgi.h"
  25. #include "xs_html.h"
  26. #include "xs_po.h"
  27. #include "xs_webmention.h"
  28. #include "xs_list_tools.h"
  29. #include "snac.h"
  30. #include <sys/time.h>
  31. #include <sys/stat.h>
  32. #include <sys/wait.h>
  33. #include <limits.h>
  34. #include <stdio.h>
  35. #include <stdlib.h>
  36. #include <unistd.h>
  37. xs_str *srv_basedir = NULL;
  38. xs_dict *srv_config = NULL;
  39. xs_str *srv_baseurl = NULL;
  40. xs_str *srv_proxy_token_seed = NULL;
  41. xs_dict *srv_langs = NULL;
  42. const char *months[12] = {0};
  43. int dbglevel = 0;
  44. int mkdirx(const char *pathname)
  45. /* creates a directory with special permissions */
  46. {
  47. int ret;
  48. if ((ret = mkdir(pathname, DIR_PERM)) != -1) {
  49. /* try to the set the setgid bit, to allow system users
  50. to create files in these directories using the
  51. command-line tool. This may fail in some restricted
  52. environments, but it's of no use there anyway */
  53. chmod(pathname, DIR_PERM_ADD);
  54. }
  55. return ret;
  56. }
  57. xs_str *tid(int offset)
  58. /* returns a time-based Id */
  59. {
  60. struct timeval tv;
  61. gettimeofday(&tv, NULL);
  62. return xs_fmt("%010ld.%06ld", (long)tv.tv_sec + (long)offset, (long)tv.tv_usec);
  63. }
  64. double ftime(void)
  65. /* returns the UNIX time as a float */
  66. {
  67. xs *ntid = tid(0);
  68. return atof(ntid);
  69. }
  70. xs_str *rfctime(int offset, struct timeval *tv)
  71. /* returns an RFC3339 format (with fractional seconds) compliant with Mastodon datetime */
  72. {
  73. struct timeval tv2;
  74. if (tv == NULL) {
  75. gettimeofday(&tv2, NULL);
  76. tv = &tv2;
  77. }
  78. xs_str *iso_date = xs_str_utctime((long)(*tv).tv_sec + (long)offset, "%Y-%m-%dT%H:%M:%S");
  79. xs_str *date_with_ms =xs_fmt("%s.%03ldZ", iso_date, (long)(*tv).tv_usec/1000L);
  80. xs_free(iso_date);
  81. return date_with_ms;
  82. }
  83. int validate_uid(const char *uid)
  84. /* returns if uid is a valid identifier */
  85. {
  86. if (!uid || *uid == '\0')
  87. return 0;
  88. while (*uid) {
  89. if (!(isalnum((int)*uid) || *uid == '_'))
  90. return 0;
  91. uid++;
  92. }
  93. return 1;
  94. }
  95. void srv_log(xs_str *str)
  96. /* logs a debug message */
  97. {
  98. if (xs_str_in(str, srv_basedir) != -1) {
  99. /* replace basedir with ~ */
  100. str = xs_replace_i(str, srv_basedir, "~");
  101. }
  102. xs *tm = xs_str_localtime(0, "%H:%M:%S");
  103. fprintf(stderr, "%s %s\n", tm, str);
  104. /* if the ~/log/ folder exists, also write to a file there */
  105. xs *dt = xs_str_localtime(0, "%Y-%m-%d");
  106. xs *lf = xs_fmt("%s/log/%s.log", srv_basedir, dt);
  107. FILE *f;
  108. if ((f = fopen(lf, "a")) != NULL) {
  109. fprintf(f, "%s %s\n", tm, str);
  110. fclose(f);
  111. }
  112. xs_free(str);
  113. }
  114. void snac_log(const snac *snac, xs_str *str)
  115. /* prints a user debugging information */
  116. {
  117. xs *o_str = str;
  118. xs_str *msg = xs_fmt("[%s] %s", snac->uid, o_str);
  119. if (xs_str_in(msg, snac->basedir) != -1) {
  120. /* replace long basedir references with ~ */
  121. msg = xs_replace_i(msg, snac->basedir, "~");
  122. }
  123. srv_log(msg);
  124. }
  125. xs_str *hash_password(const char *uid, const char *passwd, const char *nonce)
  126. /* hashes a password */
  127. {
  128. xs *d_nonce = NULL;
  129. xs *combi;
  130. xs *hash;
  131. if (nonce == NULL) {
  132. unsigned int r;
  133. xs_rnd_buf(&r, sizeof(r));
  134. d_nonce = xs_fmt("%08x", r);
  135. nonce = d_nonce;
  136. }
  137. combi = xs_fmt("%s:%s:%s", nonce, uid, passwd);
  138. hash = xs_sha1_hex(combi, strlen(combi));
  139. return xs_fmt("%s:%s", nonce, hash);
  140. }
  141. int check_password(const char *uid, const char *passwd, const char *hash)
  142. /* checks a password */
  143. {
  144. int ret = 0;
  145. xs *spl = xs_split_n(hash, ":", 1);
  146. if (xs_list_len(spl) == 2) {
  147. xs *n_hash = hash_password(uid, passwd, xs_list_get(spl, 0));
  148. ret = (strcmp(hash, n_hash) == 0);
  149. }
  150. return ret;
  151. }
  152. char* findprog(const char *prog)
  153. /* find a prog in PATH and return the first match */
  154. {
  155. char *path_env, *path, *dir, *filename;
  156. int len;
  157. size_t filename_sz;
  158. struct stat sbuf;
  159. path_env = getenv("PATH");
  160. if (!prog || !path_env)
  161. return NULL;
  162. path_env = strdup(path_env);
  163. if (!path_env)
  164. return NULL;
  165. path = path_env;
  166. filename_sz = strlen(path) + strlen(prog) + 2;
  167. filename = xs_realloc(NULL, filename_sz);
  168. while ((dir = strsep(&path, ":")) != NULL) {
  169. /* empty entries as ./ instead of / */
  170. if (*dir == '\0')
  171. dir = ".";
  172. /* strip trailing / */
  173. len = strlen(dir);
  174. while (len > 0 && dir[len-1] == '/')
  175. dir[--len] = '\0';
  176. len = snprintf(filename, filename_sz, "%s/%s", dir, prog);
  177. if (len > 0 && len < (int) filename_sz &&
  178. (stat(filename, &sbuf) == 0) && S_ISREG(sbuf.st_mode) &&
  179. access(filename, X_OK) == 0) {
  180. free(path_env);
  181. return filename;
  182. }
  183. }
  184. free(path_env);
  185. xs_free(filename);
  186. return NULL;
  187. }
  188. int strip_media(const char *fn)
  189. /* strips EXIF data from a file */
  190. {
  191. int ret = 0;
  192. const xs_val *v = xs_dict_get(srv_config, "strip_exif");
  193. if (xs_type(v) == XSTYPE_TRUE) {
  194. /* Heuristic: find 'user/' in the path to make it relative */
  195. /* This works for ~/user/..., /var/snac/user/..., etc. */
  196. const char *r_fn = strstr(fn, "user/");
  197. if (r_fn == NULL) {
  198. /* Fallback: try to strip ~/ if present */
  199. if (strncmp(fn, "~/", 2) == 0)
  200. r_fn = fn + 2;
  201. else
  202. r_fn = fn;
  203. }
  204. xs *l_fn = xs_tolower_i(xs_dup(r_fn));
  205. /* check image extensions */
  206. if (xs_endswith(l_fn, ".jpg") || xs_endswith(l_fn, ".jpeg") ||
  207. xs_endswith(l_fn, ".png") || xs_endswith(l_fn, ".webp") ||
  208. xs_endswith(l_fn, ".heic") || xs_endswith(l_fn, ".heif") ||
  209. xs_endswith(l_fn, ".avif") || xs_endswith(l_fn, ".tiff") ||
  210. xs_endswith(l_fn, ".gif") || xs_endswith(l_fn, ".bmp")) {
  211. const char *mp = xs_dict_get(srv_config, "mogrify_path");
  212. pid_t pid = fork();
  213. if (pid == -1) {
  214. srv_log(xs_fmt("strip_media: cannot fork()"));
  215. return -1;
  216. } else if (pid == 0) {
  217. chdir(srv_basedir);
  218. execl(mp, "-auto-orient", "-strip", r_fn, (char*) NULL);
  219. _exit(1);
  220. }
  221. if (waitpid(pid, &ret, 0) == -1) {
  222. srv_log(xs_fmt("strip_media: cannot waitpid()"));
  223. return -1;
  224. }
  225. if (ret != 0)
  226. srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
  227. else
  228. srv_debug(1, xs_fmt("strip_media: stripped %s", r_fn));
  229. }
  230. else
  231. /* check video extensions */
  232. if (xs_endswith(l_fn, ".mp4") || xs_endswith(l_fn, ".m4v") ||
  233. xs_endswith(l_fn, ".mov") || xs_endswith(l_fn, ".webm") ||
  234. xs_endswith(l_fn, ".mkv") || xs_endswith(l_fn, ".avi")) {
  235. const char *fp = xs_dict_get(srv_config, "ffmpeg_path");
  236. /* ffmpeg cannot modify in-place, so we need a temp file */
  237. /* we must preserve valid extension for ffmpeg to guess the format */
  238. const char *ext = strrchr(r_fn, '.');
  239. if (ext == NULL) ext = "";
  240. xs *tmp_fn = xs_fmt("%s.tmp%s", r_fn, ext);
  241. pid_t pid = fork();
  242. if (pid == -1) {
  243. srv_log(xs_fmt("strip_media: cannot fork()"));
  244. return -1;
  245. } else if (pid == 0) {
  246. chdir(srv_basedir);
  247. /* -map_metadata -1 strips all global metadata */
  248. /* -c copy copies input streams without re-encoding */
  249. execl(fp, "-y", "-i", r_fn, "-map_metadata", "-1", "-c", "copy", tmp_fn, (char*) NULL);
  250. _exit(1);
  251. }
  252. if (waitpid(pid, &ret, 0) == -1) {
  253. srv_log(xs_fmt("strip_media: cannot waitpid()"));
  254. return -1;
  255. }
  256. if (ret != 0) {
  257. srv_log(xs_fmt("strip_media: error stripping %s %d", r_fn, ret));
  258. /* try to cleanup, just in case */
  259. /* unlink needs full path too if we are not in basedir */
  260. xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
  261. unlink(full_tmp_fn);
  262. }
  263. else {
  264. /* rename tmp file to original */
  265. /* use full path for source because it was created relative to basedir */
  266. xs *full_tmp_fn = xs_fmt("%s/%s", srv_basedir, tmp_fn);
  267. if (rename(full_tmp_fn, fn) == 0)
  268. srv_debug(1, xs_fmt("strip_media: stripped %s", fn));
  269. else
  270. srv_log(xs_fmt("strip_media: error renaming %s to %s", full_tmp_fn, fn));
  271. }
  272. }
  273. }
  274. return ret;
  275. }
  276. int check_strip_tool(void)
  277. /* check if strip_exif tools do exist and fix their absolute path */
  278. {
  279. const xs_val *v = xs_dict_get(srv_config, "strip_exif");
  280. /* skip if unless strip_exif; return non-error */
  281. if (xs_type(v) != XSTYPE_TRUE)
  282. return 1;
  283. int ret = 1;
  284. const char *progs[] = { "ffmpeg", "mogrify" };
  285. for (int i = 0; i < (int)(sizeof(progs) / sizeof(progs[0])); i++) {
  286. xs_str *key = xs_fmt("%s_path", progs[i]);
  287. const char *val = xs_dict_get(srv_config, key);
  288. if (val == NULL) {
  289. val = findprog(progs[i]);
  290. if (val != NULL)
  291. srv_debug(1, xs_fmt("check_strip_tool: found %s in PATH at %s", progs[i], val));
  292. }
  293. if (val == NULL) {
  294. srv_log(xs_fmt("check_strip_tool: %s not found in PATH", progs[i]));
  295. ret = 0;
  296. } else if (access(val, X_OK) != 0) {
  297. srv_log(xs_fmt("check_strip_tool: %s '%s' is not executable", progs[i], val));
  298. ret = 0;
  299. } else {
  300. srv_config = xs_dict_set(srv_config, key, val);
  301. }
  302. xs_free(key);
  303. }
  304. return ret;
  305. }