1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 
3 #include <errno.h>
4 #include <stdio.h>
5 #include <unistd.h>
6 
7 #include "xdg-autostart-service.h"
8 
9 #include "conf-parser.h"
10 #include "escape.h"
11 #include "unit-name.h"
12 #include "path-util.h"
13 #include "fd-util.h"
14 #include "generator.h"
15 #include "log.h"
16 #include "specifier.h"
17 #include "string-util.h"
18 #include "nulstr-util.h"
19 #include "strv.h"
20 
xdg_autostart_service_free(XdgAutostartService * s)21 XdgAutostartService* xdg_autostart_service_free(XdgAutostartService *s) {
22         if (!s)
23                 return NULL;
24 
25         free(s->name);
26         free(s->path);
27         free(s->description);
28 
29         free(s->type);
30         free(s->exec_string);
31         free(s->working_directory);
32 
33         strv_free(s->only_show_in);
34         strv_free(s->not_show_in);
35 
36         free(s->try_exec);
37         free(s->autostart_condition);
38         free(s->kde_autostart_condition);
39 
40         free(s->gnome_autostart_phase);
41 
42         return mfree(s);
43 }
44 
xdg_autostart_service_translate_name(const char * name)45 char *xdg_autostart_service_translate_name(const char *name) {
46         _cleanup_free_ char *c = NULL, *escaped = NULL;
47         char *res;
48 
49         c = strdup(name);
50         if (!c)
51                 return NULL;
52 
53         res = endswith(c, ".desktop");
54         if (res)
55                 *res = '\0';
56 
57         escaped = unit_name_escape(c);
58         if (!escaped)
59                 return NULL;
60 
61         return strjoin("app-", escaped, "@autostart.service");
62 }
63 
xdg_config_parse_bool(const char * unit,const char * filename,unsigned line,const char * section,unsigned section_line,const char * lvalue,int ltype,const char * rvalue,void * data,void * userdata)64 static int xdg_config_parse_bool(
65                 const char *unit,
66                 const char *filename,
67                 unsigned line,
68                 const char *section,
69                 unsigned section_line,
70                 const char *lvalue,
71                 int ltype,
72                 const char *rvalue,
73                 void *data,
74                 void *userdata) {
75 
76         bool *b = data;
77 
78         assert(filename);
79         assert(lvalue);
80         assert(rvalue);
81         assert(data);
82 
83         if (streq(rvalue, "true"))
84                 *b = true;
85         else if (streq(rvalue, "false"))
86                 *b = false;
87         else
88                 return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), "Invalid value for boolean: %s", rvalue);
89 
90         return 0;
91 }
92 
93 /* Unescapes the string in-place, returns non-zero status on error. */
xdg_unescape_string(const char * unit,const char * filename,int line,char * str)94 static int xdg_unescape_string(
95                 const char *unit,
96                 const char *filename,
97                 int line,
98                 char *str) {
99 
100         char *in;
101         char *out;
102 
103         assert(str);
104 
105         in = out = str;
106 
107         for (; *in; in++, out++) {
108                 if (*in == '\\') {
109                         /* Move forward, and ensure it is a valid escape. */
110                         in++;
111 
112                         switch (*in) {
113                                 case 's':
114                                         *out = ' ';
115                                         break;
116                                 case 'n':
117                                         *out = '\n';
118                                         break;
119                                 case 't':
120                                         *out = '\t';
121                                         break;
122                                 case 'r':
123                                         *out = '\r';
124                                         break;
125                                 case '\\':
126                                         *out = '\\';
127                                         break;
128                                 case ';':
129                                         /* Technically only permitted for strv. */
130                                         *out = ';';
131                                         break;
132                                 default:
133                                         return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), "Undefined escape sequence \\%c.", *in);
134                         }
135 
136                         continue;
137                 }
138 
139                 *out = *in;
140         }
141         *out = '\0';
142 
143         return 0;
144 }
145 
146 /* Note: We do not bother with unescaping the strings, hence the _raw postfix. */
xdg_config_parse_string(const char * unit,const char * filename,unsigned line,const char * section,unsigned section_line,const char * lvalue,int ltype,const char * rvalue,void * data,void * userdata)147 static int xdg_config_parse_string(
148                 const char *unit,
149                 const char *filename,
150                 unsigned line,
151                 const char *section,
152                 unsigned section_line,
153                 const char *lvalue,
154                 int ltype,
155                 const char *rvalue,
156                 void *data,
157                 void *userdata) {
158 
159         _cleanup_free_ char *res = NULL;
160         char **out = data;
161         int r;
162 
163         assert(filename);
164         assert(lvalue);
165         assert(rvalue);
166         assert(data);
167 
168         /* XDG does not allow duplicate definitions. */
169         if (*out) {
170                 log_syntax(unit, LOG_WARNING, filename, line, 0, "Key %s was defined multiple times, ignoring.", lvalue);
171                 return 0;
172         }
173 
174         res = strdup(rvalue);
175         if (!res)
176                 return log_oom();
177 
178         r = xdg_unescape_string(unit, filename, line, res);
179         if (r < 0)
180                 return r;
181 
182         *out = TAKE_PTR(res);
183         return 0;
184 }
185 
strv_strndup_unescape_and_push(const char * unit,const char * filename,unsigned line,char *** sv,size_t * n,const char * start,const char * end)186 static int strv_strndup_unescape_and_push(
187                 const char *unit,
188                 const char *filename,
189                 unsigned line,
190                 char ***sv,
191                 size_t *n,
192                 const char *start,
193                 const char *end) {
194 
195         if (end == start)
196                 return 0;
197 
198         _cleanup_free_ char *copy = NULL;
199         int r;
200 
201         copy = strndup(start, end - start);
202         if (!copy)
203                 return log_oom();
204 
205         r = xdg_unescape_string(unit, filename, line, copy);
206         if (r < 0)
207                 return r;
208 
209         if (!GREEDY_REALLOC(*sv, *n + 2)) /* One extra for NULL */
210                 return log_oom();
211 
212         (*sv)[*n] = TAKE_PTR(copy);
213         (*sv)[*n + 1] = NULL;
214         (*n)++;
215 
216         return 0;
217 }
218 
xdg_config_parse_strv(const char * unit,const char * filename,unsigned line,const char * section,unsigned section_line,const char * lvalue,int ltype,const char * rvalue,void * data,void * userdata)219 static int xdg_config_parse_strv(
220                 const char *unit,
221                 const char *filename,
222                 unsigned line,
223                 const char *section,
224                 unsigned section_line,
225                 const char *lvalue,
226                 int ltype,
227                 const char *rvalue,
228                 void *data,
229                 void *userdata) {
230 
231         char ***ret_sv = data;
232         int r;
233 
234         assert(filename);
235         assert(lvalue);
236         assert(rvalue);
237         assert(data);
238 
239         /* XDG does not allow duplicate definitions. */
240         if (*ret_sv) {
241                 log_syntax(unit, LOG_WARNING, filename, line, 0, "Key %s was already defined, ignoring.", lvalue);
242                 return 0;
243         }
244 
245         size_t n = 0;
246         _cleanup_strv_free_ char **sv = NULL;
247 
248         if (!GREEDY_REALLOC0(sv, 1))
249                 return log_oom();
250 
251         /* We cannot use strv_split because it does not handle escaping correctly. */
252         const char *start = rvalue, *end;
253 
254         for (end = start; *end; end++) {
255                 if (*end == '\\') {
256                         /* Move forward, and ensure it is a valid escape. */
257                         end++;
258                         if (!strchr("sntr\\;", *end)) {
259                                 log_syntax(unit, LOG_WARNING, filename, line, 0, "Undefined escape sequence \\%c.", *end);
260                                 return 0;
261                         }
262                         continue;
263                 }
264 
265                 if (*end == ';') {
266                         r = strv_strndup_unescape_and_push(unit, filename, line,
267                                                            &sv, &n,
268                                                            start, end);
269                         if (r < 0)
270                                 return r;
271 
272                         start = end + 1;
273                 }
274         }
275 
276         /* Handle the trailing entry after the last separator */
277         r = strv_strndup_unescape_and_push(unit, filename, line,
278                                            &sv, &n,
279                                            start, end);
280         if (r < 0)
281                 return r;
282 
283         *ret_sv = TAKE_PTR(sv);
284         return 0;
285 }
286 
xdg_config_item_table_lookup(const void * table,const char * section,const char * lvalue,ConfigParserCallback * ret_func,int * ret_ltype,void ** ret_data,void * userdata)287 static int xdg_config_item_table_lookup(
288                 const void *table,
289                 const char *section,
290                 const char *lvalue,
291                 ConfigParserCallback *ret_func,
292                 int *ret_ltype,
293                 void **ret_data,
294                 void *userdata) {
295 
296         assert(lvalue);
297 
298         /* Ignore any keys with [] as those are translations. */
299         if (strchr(lvalue, '[')) {
300                 *ret_func = NULL;
301                 *ret_ltype = 0;
302                 *ret_data = NULL;
303                 return 1;
304         }
305 
306         return config_item_table_lookup(table, section, lvalue, ret_func, ret_ltype, ret_data, userdata);
307 }
308 
xdg_autostart_service_parse_desktop(const char * path)309 XdgAutostartService *xdg_autostart_service_parse_desktop(const char *path) {
310         _cleanup_(xdg_autostart_service_freep) XdgAutostartService *service = NULL;
311         int r;
312 
313         service = new0(XdgAutostartService, 1);
314         if (!service)
315                 return NULL;
316 
317         service->path = strdup(path);
318         if (!service->path)
319                 return NULL;
320 
321         const ConfigTableItem items[] = {
322                 { "Desktop Entry", "Name",                      xdg_config_parse_string, 0, &service->description             },
323                 { "Desktop Entry", "Exec",                      xdg_config_parse_string, 0, &service->exec_string             },
324                 { "Desktop Entry", "Path",                      xdg_config_parse_string, 0, &service->working_directory       },
325                 { "Desktop Entry", "TryExec",                   xdg_config_parse_string, 0, &service->try_exec                },
326                 { "Desktop Entry", "Type",                      xdg_config_parse_string, 0, &service->type                    },
327                 { "Desktop Entry", "OnlyShowIn",                xdg_config_parse_strv,   0, &service->only_show_in            },
328                 { "Desktop Entry", "NotShowIn",                 xdg_config_parse_strv,   0, &service->not_show_in             },
329                 { "Desktop Entry", "Hidden",                    xdg_config_parse_bool,   0, &service->hidden                  },
330                 { "Desktop Entry", "AutostartCondition",        xdg_config_parse_string, 0, &service->autostart_condition     },
331                 { "Desktop Entry", "X-KDE-autostart-condition", xdg_config_parse_string, 0, &service->kde_autostart_condition },
332                 { "Desktop Entry", "X-GNOME-Autostart-Phase",   xdg_config_parse_string, 0, &service->gnome_autostart_phase   },
333                 { "Desktop Entry", "X-systemd-skip",            xdg_config_parse_bool,   0, &service->systemd_skip            },
334 
335                 /* Common entries that we do not use currently. */
336                 { "Desktop Entry", "Categories",                NULL, 0, NULL},
337                 { "Desktop Entry", "Comment",                   NULL, 0, NULL},
338                 { "Desktop Entry", "DBusActivatable",           NULL, 0, NULL},
339                 { "Desktop Entry", "Encoding",                  NULL, 0, NULL},
340                 { "Desktop Entry", "GenericName",               NULL, 0, NULL},
341                 { "Desktop Entry", "Icon",                      NULL, 0, NULL},
342                 { "Desktop Entry", "Keywords",                  NULL, 0, NULL},
343                 { "Desktop Entry", "MimeType",                  NULL, 0, NULL},
344                 { "Desktop Entry", "NoDisplay",                 NULL, 0, NULL},
345                 { "Desktop Entry", "StartupNotify",             NULL, 0, NULL},
346                 { "Desktop Entry", "StartupWMClass",            NULL, 0, NULL},
347                 { "Desktop Entry", "Terminal",                  NULL, 0, NULL},
348                 { "Desktop Entry", "URL",                       NULL, 0, NULL},
349                 { "Desktop Entry", "Version",                   NULL, 0, NULL},
350                 {}
351         };
352 
353         r = config_parse(NULL, service->path, NULL,
354                          "Desktop Entry\0",
355                          xdg_config_item_table_lookup, items,
356                          CONFIG_PARSE_WARN, service,
357                          NULL);
358         /* If parsing failed, only hide the file so it will still mask others. */
359         if (r < 0) {
360                 log_warning_errno(r, "Failed to parse %s, ignoring it", service->path);
361                 service->hidden = true;
362         }
363 
364         return TAKE_PTR(service);
365 }
366 
xdg_autostart_format_exec_start(const char * exec,char ** ret_exec_start)367 int xdg_autostart_format_exec_start(
368                 const char *exec,
369                 char **ret_exec_start) {
370 
371         _cleanup_strv_free_ char **exec_split = NULL;
372         char *res;
373         size_t n, i;
374         bool first_arg;
375         int r;
376 
377         /*
378          * Unfortunately, there is a mismatch between systemd's idea of $PATH and XDGs. I.e. we need to
379          * ensure that we have an absolute path to support cases where $PATH has been modified from the
380          * default set.
381          *
382          * Note that this is only needed for development environments though; so while it is important, this
383          * should have no effect in production environments.
384          *
385          * To be compliant with the XDG specification, we also need to strip certain parameters and
386          * such. Doing so properly makes parsing the command line unavoidable.
387          *
388          * NOTE: Technically, XDG only specifies " as quotes, while this also accepts '.
389          */
390         r = strv_split_full(&exec_split, exec, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX);
391         if (r < 0)
392                 return r;
393 
394         if (strv_isempty(exec_split))
395                 return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Exec line is empty");
396 
397         first_arg = true;
398         for (i = n = 0; exec_split[i]; i++) {
399                 _cleanup_free_ char *c = NULL, *raw = NULL, *percent = NULL;
400                 ssize_t l;
401 
402                 l = cunescape(exec_split[i], 0, &c);
403                 if (l < 0)
404                         return log_debug_errno(l, "Failed to unescape '%s': %m", exec_split[i]);
405 
406                 if (first_arg) {
407                         _cleanup_free_ char *executable = NULL;
408 
409                         /* This is the executable, find it in $PATH */
410                         first_arg = false;
411                         r = find_executable(c, &executable);
412                         if (r < 0)
413                                 return log_info_errno(r, "Exec binary '%s' does not exist: %m", c);
414 
415                         free_and_replace(exec_split[n++], executable);
416                         continue;
417                 }
418 
419                 /*
420                  * Remove any standardised XDG fields; we assume they never appear as part of another
421                  * argument as that just does not make any sense as they can be empty (GLib will e.g. turn
422                  * "%f" into an empty argument).  Other implementations may handle this differently.
423                  */
424                 if (STR_IN_SET(c,
425                                "%f", "%F",
426                                "%u", "%U",
427                                "%d", "%D",
428                                "%n", "%N",
429                                "%i",          /* Location of icon, could be implemented. */
430                                "%c",          /* Translated application name, could be implemented. */
431                                "%k",          /* Location of desktop file, could be implemented. */
432                                "%v",
433                                "%m"
434                                ))
435                         continue;
436 
437                 /*
438                  * %% -> % and then % -> %% means that we correctly quote any % and also quote any left over
439                  * (and invalid) % specifier from the desktop file.
440                  */
441                 raw = strreplace(c, "%%", "%");
442                 if (!raw)
443                         return log_oom();
444                 percent = strreplace(raw, "%", "%%");
445                 if (!percent)
446                         return log_oom();
447 
448                 free_and_replace(exec_split[n++], percent);
449         }
450         for (; exec_split[n]; n++)
451                 exec_split[n] = mfree(exec_split[n]);
452 
453         res = quote_command_line(exec_split, SHELL_ESCAPE_EMPTY);
454         if (!res)
455                 return log_oom();
456 
457         *ret_exec_start = res;
458         return 0;
459 }
460 
xdg_autostart_generate_desktop_condition(const XdgAutostartService * service,FILE * f,const char * test_binary,const char * condition)461 static int xdg_autostart_generate_desktop_condition(
462                 const XdgAutostartService *service,
463                 FILE *f,
464                 const char *test_binary,
465                 const char *condition) {
466 
467         int r;
468 
469         /* Generate an ExecCondition for GNOME autostart condition */
470         if (!isempty(condition)) {
471                 _cleanup_free_ char *gnome_autostart_condition_path = NULL, *e_autostart_condition = NULL;
472 
473                 r = find_executable(test_binary, &gnome_autostart_condition_path);
474                 if (r < 0) {
475                         log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
476                                        "%s: ExecCondition executable %s not found, unit will not be started automatically: %m",
477                                        service->path, test_binary);
478                         fprintf(f, "# ExecCondition using %s skipped due to missing binary.\n", test_binary);
479                         return 0;
480                 }
481 
482                 e_autostart_condition = cescape(condition);
483                 if (!e_autostart_condition)
484                         return log_oom();
485 
486                 log_debug("%s: ExecCondition converted to %s --condition \"%s\"…",
487                           service->path, gnome_autostart_condition_path, e_autostart_condition);
488 
489                 fprintf(f,
490                          "ExecCondition=%s --condition \"%s\"\n",
491                          gnome_autostart_condition_path,
492                          e_autostart_condition);
493         }
494 
495         return 0;
496 }
497 
xdg_autostart_service_generate_unit(const XdgAutostartService * service,const char * dest)498 int xdg_autostart_service_generate_unit(
499                 const XdgAutostartService *service,
500                 const char *dest) {
501 
502         _cleanup_free_ char *path_escaped = NULL, *exec_start = NULL, *unit = NULL;
503         _cleanup_fclose_ FILE *f = NULL;
504         int r;
505 
506         assert(service);
507 
508         /* Nothing to do for hidden services. */
509         if (service->hidden) {
510                 log_debug("%s: not generating unit, entry is hidden.", service->path);
511                 return 0;
512         }
513 
514         if (service->systemd_skip) {
515                 log_debug("%s: not generating unit, marked as skipped by generator.", service->path);
516                 return 0;
517         }
518 
519         /* Nothing to do if type is not Application. */
520         if (!streq_ptr(service->type, "Application")) {
521                 log_debug("%s: not generating unit, Type=%s is not supported.", service->path, service->type);
522                 return 0;
523         }
524 
525         if (!service->exec_string) {
526                 log_warning("%s: not generating unit, no Exec= line.", service->path);
527                 return 0;
528         }
529 
530         /* The TryExec key cannot be checked properly from the systemd unit, it is trivial to check using
531          * find_executable though. */
532         if (service->try_exec) {
533                 r = find_executable(service->try_exec, NULL);
534                 if (r < 0) {
535                         log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
536                                        "%s: not generating unit, could not find TryExec= binary %s: %m",
537                                        service->path, service->try_exec);
538                         return 0;
539                 }
540         }
541 
542         r = xdg_autostart_format_exec_start(service->exec_string, &exec_start);
543         if (r < 0) {
544                 log_warning_errno(r, "%s: not generating unit, error parsing Exec= line: %m", service->path);
545                 return 0;
546         }
547 
548         if (service->gnome_autostart_phase) {
549                 /* There is no explicit value for the "Application" phase. */
550                 log_debug("%s: not generating unit, startup phases are not supported.", service->path);
551                 return 0;
552         }
553 
554         path_escaped = specifier_escape(service->path);
555         if (!path_escaped)
556                 return log_oom();
557 
558         unit = path_join(dest, service->name);
559         if (!unit)
560                 return log_oom();
561 
562         f = fopen(unit, "wxe");
563         if (!f)
564                 return log_error_errno(errno, "%s: failed to create unit file %s: %m", service->path, unit);
565 
566         fprintf(f,
567                 "# Automatically generated by systemd-xdg-autostart-generator\n\n"
568                 "[Unit]\n"
569                 "Documentation=man:systemd-xdg-autostart-generator(8)\n"
570                 "SourcePath=%s\n"
571                 "PartOf=graphical-session.target\n\n",
572                 path_escaped);
573 
574         if (service->description) {
575                 _cleanup_free_ char *t = NULL;
576 
577                 t = specifier_escape(service->description);
578                 if (!t)
579                         return log_oom();
580 
581                 fprintf(f, "Description=%s\n", t);
582         }
583 
584         /* Only start after the session is ready. */
585         fprintf(f,
586                 "After=graphical-session.target\n");
587 
588         fprintf(f,
589                 "\n[Service]\n"
590                 "Type=exec\n"
591                 "ExitType=cgroup\n"
592                 "ExecStart=:%s\n"
593                 "Restart=no\n"
594                 "TimeoutSec=5s\n"
595                 "Slice=app.slice\n",
596                 exec_start);
597 
598         if (service->working_directory) {
599                 _cleanup_free_ char *e_working_directory = NULL;
600 
601                 e_working_directory = cescape(service->working_directory);
602                 if (!e_working_directory)
603                         return log_oom();
604 
605                 fprintf(f, "WorkingDirectory=-%s\n", e_working_directory);
606         }
607 
608         /* Generate an ExecCondition to check $XDG_CURRENT_DESKTOP */
609         if (!strv_isempty(service->only_show_in) || !strv_isempty(service->not_show_in)) {
610                 _cleanup_free_ char *only_show_in = NULL, *not_show_in = NULL, *e_only_show_in = NULL, *e_not_show_in = NULL;
611 
612                 only_show_in = strv_join(service->only_show_in, ":");
613                 not_show_in = strv_join(service->not_show_in, ":");
614                 if (!only_show_in || !not_show_in)
615                         return log_oom();
616 
617                 e_only_show_in = cescape(only_show_in);
618                 e_not_show_in = cescape(not_show_in);
619                 if (!e_only_show_in || !e_not_show_in)
620                         return log_oom();
621 
622                 /* Just assume the values are reasonably sane */
623                 fprintf(f,
624                         "ExecCondition=" ROOTLIBEXECDIR "/systemd-xdg-autostart-condition \"%s\" \"%s\"\n",
625                         e_only_show_in,
626                         e_not_show_in);
627         }
628 
629         r = xdg_autostart_generate_desktop_condition(service, f,
630                                                      "gnome-systemd-autostart-condition",
631                                                      service->autostart_condition);
632         if (r < 0)
633                 return r;
634 
635         r = xdg_autostart_generate_desktop_condition(service, f,
636                                                      "kde-systemd-start-condition",
637                                                      service->kde_autostart_condition);
638         if (r < 0)
639                 return r;
640 
641         log_debug("%s: symlinking %s in xdg-desktop-autostart.target/.wants…", service->path, service->name);
642         return generator_add_symlink(dest, "xdg-desktop-autostart.target", "wants", service->name);
643 }
644