chiark / gitweb /
mechanisms: add mechanisms to change system locale and clock
[elogind.git] / src / timedated.c
1 /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
2
3 /***
4   This file is part of systemd.
5
6   Copyright 2011 Lennart Poettering
7
8   systemd is free software; you can redistribute it and/or modify it
9   under the terms of the GNU General Public License as published by
10   the Free Software Foundation; either version 2 of the License, or
11   (at your option) any later version.
12
13   systemd is distributed in the hope that it will be useful, but
14   WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   General Public License for more details.
17
18   You should have received a copy of the GNU General Public License
19   along with systemd; If not, see <http://www.gnu.org/licenses/>.
20 ***/
21
22 #include <dbus/dbus.h>
23
24 #include <errno.h>
25 #include <string.h>
26 #include <unistd.h>
27
28 #include "util.h"
29 #include "strv.h"
30 #include "dbus-common.h"
31 #include "polkit.h"
32
33 #define NULL_ADJTIME_UTC "0.0 0 0\n0\nUTC\n"
34 #define NULL_ADJTIME_LOCAL "0.0 0 0\n0\nLOCAL\n"
35
36 #define INTROSPECTION                                                   \
37         DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE                       \
38         "<node>\n"                                                      \
39         " <interface name=\"org.freedesktop.timedate1\">\n"             \
40         "  <property name=\"Timezone\" type=\"s\" access=\"read\"/>\n"  \
41         "  <property name=\"LocalRTC\" type=\"b\" access=\"read\"/>\n"  \
42         "  <method name=\"SetTime\">\n"                                 \
43         "   <arg name=\"usec_utc\" type=\"x\" direction=\"in\"/>\n"     \
44         "   <arg name=\"relative\" type=\"b\" direction=\"in\"/>\n"     \
45         "   <arg name=\"user_interaction\" type=\"b\" direction=\"in\"/>\n" \
46         "  </method>\n"                                                 \
47         "  <method name=\"SetTimezone\">\n"                             \
48         "   <arg name=\"timezone\" type=\"s\" direction=\"in\"/>\n"     \
49         "   <arg name=\"user_interaction\" type=\"b\" direction=\"in\"/>\n" \
50         "  </method>\n"                                                 \
51         "  <method name=\"SetLocalRTC\">\n"                             \
52         "   <arg name=\"local_rtc\" type=\"b\" direction=\"in\"/>\n"    \
53         "   <arg name=\"user_interaction\" type=\"b\" direction=\"in\"/>\n" \
54         "  </method>\n"                                                 \
55         " </interface>\n"                                               \
56         BUS_PROPERTIES_INTERFACE                                        \
57         BUS_INTROSPECTABLE_INTERFACE                                    \
58         BUS_PEER_INTERFACE                                              \
59         "</node>\n"
60
61 #define INTERFACES_LIST                         \
62         BUS_GENERIC_INTERFACES_LIST             \
63         "org.freedesktop.locale1\0"
64
65 static char *zone = NULL;
66 static bool local_rtc = false;
67
68 static void free_data(void) {
69         free(zone);
70         zone = NULL;
71
72         local_rtc = false;
73 }
74
75 static bool valid_timezone(const char *name) {
76         const char *p;
77         char *t;
78         bool slash = false;
79         int r;
80         struct stat st;
81
82         assert(name);
83
84         if (*name == '/' || *name == 0)
85                 return false;
86
87         for (p = name; *p; p++) {
88                 if (!(*p >= '0' && *p <= '9') &&
89                     !(*p >= 'a' && *p <= 'z') &&
90                     !(*p >= 'A' && *p <= 'Z') &&
91                     !(*p == '-' || *p == '_' || *p == '+' || *p == '/'))
92                         return false;
93
94                 if (*p == '/') {
95
96                         if (slash)
97                                 return false;
98
99                         slash = true;
100                 } else
101                         slash = false;
102         }
103
104         if (slash)
105                 return false;
106
107         t = strappend("/usr/share/zoneinfo/", name);
108         if (!t)
109                 return false;
110
111         r = stat(t, &st);
112         free(t);
113
114         if (r < 0)
115                 return false;
116
117         if (!S_ISREG(st.st_mode))
118                 return false;
119
120         return true;
121 }
122
123 static void verify_timezone(void) {
124         char *p, *a = NULL, *b = NULL;
125         size_t l, q;
126         int j, k;
127
128         if (!zone)
129                 return;
130
131         p = strappend("/usr/share/zoneinfo/", zone);
132         if (!p) {
133                 log_error("Out of memory");
134                 return;
135         }
136
137         j = read_full_file("/etc/localtime", &a, &l);
138         k = read_full_file(p, &b, &q);
139
140         free(p);
141
142         if (j < 0 || k < 0 || l != q || memcmp(a, b, l)) {
143                 log_warning("/etc/localtime and /etc/timezone out of sync.");
144                 free(zone);
145                 zone = NULL;
146         }
147
148         free(a);
149         free(b);
150 }
151
152 static int read_data(void) {
153         int r;
154         FILE *f;
155
156         free_data();
157
158         r = read_one_line_file("/etc/timezone", &zone);
159         if (r < 0 && r != -ENOENT)
160                 return r;
161
162         verify_timezone();
163
164         f = fopen("/etc/adjtime", "r");
165         if (f) {
166                 char line[LINE_MAX];
167                 bool b;
168
169                 b = fgets(line, sizeof(line), f) &&
170                         fgets(line, sizeof(line), f) &&
171                         fgets(line, sizeof(line), f);
172
173                 fclose(f);
174
175                 if (!b)
176                         return -EIO;
177
178                 truncate_nl(line);
179                 local_rtc = streq(line, "LOCAL");
180
181         } else if (errno != ENOENT)
182                 return -errno;
183
184         return 0;
185 }
186
187 static int write_data_timezone(void) {
188         int r = 0;
189         char *p;
190
191         if (!zone) {
192                 if (unlink("/etc/timezone") < 0 && errno != ENOENT)
193                         r = -errno;
194
195                 if (unlink("/etc/localtime") < 0 && errno != ENOENT)
196                         r = -errno;
197
198                 return r;
199         }
200
201         p = strappend("/usr/share/zoneinfo/", zone);
202         if (!p) {
203                 log_error("Out of memory");
204                 return -ENOMEM;
205         }
206
207         r = symlink_or_copy_atomic(p, "/etc/localtime");
208         free(p);
209
210         if (r < 0)
211                 return r;
212
213         r = write_one_line_file_atomic("/etc/timezone", zone);
214         if (r < 0)
215                 return r;
216
217         return 0;
218 }
219
220 static int write_data_local_rtc(void) {
221         int r;
222         char *s, *w;
223
224         r = read_full_file("/etc/adjtime", &s, NULL);
225         if (r < 0) {
226                 if (r != -ENOENT)
227                         return r;
228
229                 if (!local_rtc)
230                         return 0;
231
232                 w = strdup(NULL_ADJTIME_LOCAL);
233                 if (!w)
234                         return -ENOMEM;
235         } else {
236                 char *p, *e;
237                 size_t a, b;
238
239                 p = strchr(s, '\n');
240                 if (!p) {
241                         free(s);
242                         return -EIO;
243                 }
244
245                 p = strchr(p+1, '\n');
246                 if (!p) {
247                         free(s);
248                         return -EIO;
249                 }
250
251                 p++;
252                 e = strchr(p, '\n');
253                 if (!p) {
254                         free(s);
255                         return -EIO;
256                 }
257
258                 a = p - s;
259                 b = strlen(e);
260
261                 w = new(char, a + (local_rtc ? 5 : 3) + b + 1);
262                 if (!w) {
263                         free(s);
264                         return -ENOMEM;
265                 }
266
267                 *(char*) mempcpy(stpcpy(mempcpy(w, s, a), local_rtc ? "LOCAL" : "UTC"), e, b) = 0;
268
269                 if (streq(w, NULL_ADJTIME_UTC)) {
270                         free(w);
271
272                         if (unlink("/etc/adjtime") < 0) {
273                                 if (errno != ENOENT)
274                                         return -errno;
275                         }
276
277                         return 0;
278                 }
279         }
280
281         r = write_one_line_file_atomic("/etc/adjtime", w);
282         free(w);
283
284         return r;
285 }
286
287 static DBusHandlerResult timedate_message_handler(
288                 DBusConnection *connection,
289                 DBusMessage *message,
290                 void *userdata) {
291
292         const BusProperty properties[] = {
293                 { "org.freedesktop.timedate1", "Timezone", bus_property_append_string, "s", zone       },
294                 { "org.freedesktop.timedate1", "LocalRTC", bus_property_append_bool,   "b", &local_rtc },
295                 { NULL, NULL, NULL, NULL, NULL }
296         };
297
298         DBusMessage *reply = NULL, *changed = NULL;
299         DBusError error;
300         int r;
301
302         assert(connection);
303         assert(message);
304
305         dbus_error_init(&error);
306
307         if (dbus_message_is_method_call(message, "org.freedesktop.timedate1", "SetTimezone")) {
308                 const char *z;
309                 dbus_bool_t interactive;
310
311                 if (!dbus_message_get_args(
312                                     message,
313                                     &error,
314                                     DBUS_TYPE_STRING, &z,
315                                     DBUS_TYPE_BOOLEAN, &interactive,
316                                     DBUS_TYPE_INVALID))
317                         return bus_send_error_reply(connection, message, &error, -EINVAL);
318
319                 if (!valid_timezone(z))
320                         return bus_send_error_reply(connection, message, NULL, -EINVAL);
321
322                 if (!streq_ptr(z, zone)) {
323                         char *t;
324
325                         r = verify_polkit(connection, message, "org.freedesktop.timedate1.set-timezone", interactive, &error);
326                         if (r < 0)
327                                 return bus_send_error_reply(connection, message, &error, r);
328
329                         t = strdup(z);
330                         if (!t)
331                                 goto oom;
332
333                         free(zone);
334                         zone = t;
335
336                         r = write_data_timezone();
337                         if (r < 0) {
338                                 log_error("Failed to set timezone: %s", strerror(-r));
339                                 return bus_send_error_reply(connection, message, NULL, r);
340                         }
341
342                         log_info("Changed timezone to '%s'.", zone);
343
344                         changed = bus_properties_changed_new(
345                                         "/org/freedesktop/timedate1",
346                                         "org.freedesktop.timedate1",
347                                         "Timezone\0");
348                         if (!changed)
349                                 goto oom;
350                 }
351
352         } else if (dbus_message_is_method_call(message, "org.freedesktop.timedate1", "SetLocalRTC")) {
353                 dbus_bool_t lrtc;
354                 dbus_bool_t interactive;
355
356                 if (!dbus_message_get_args(
357                                     message,
358                                     &error,
359                                     DBUS_TYPE_BOOLEAN, &lrtc,
360                                     DBUS_TYPE_BOOLEAN, &interactive,
361                                     DBUS_TYPE_INVALID))
362                         return bus_send_error_reply(connection, message, &error, -EINVAL);
363
364                 if (lrtc != local_rtc) {
365                         r = verify_polkit(connection, message, "org.freedesktop.timedate1.set-local-rtc", interactive, &error);
366                         if (r < 0)
367                                 return bus_send_error_reply(connection, message, &error, r);
368
369                         local_rtc = lrtc;
370
371                         r = write_data_local_rtc();
372                         if (r < 0) {
373                                 log_error("Failed to set RTC to local/UTC: %s", strerror(-r));
374                                 return bus_send_error_reply(connection, message, NULL, r);
375                         }
376
377                         log_info("Changed local RTC setting to '%s'.", yes_no(local_rtc));
378
379                         changed = bus_properties_changed_new(
380                                         "/org/freedesktop/timedate1",
381                                         "org.freedesktop.timedate1",
382                                         "LocalRTC\0");
383                         if (!changed)
384                                 goto oom;
385                 }
386
387         } else if (dbus_message_is_method_call(message, "org.freedesktop.timedate1", "SetTime")) {
388                 int64_t utc;
389                 dbus_bool_t relative;
390                 dbus_bool_t interactive;
391
392                 if (!dbus_message_get_args(
393                                     message,
394                                     &error,
395                                     DBUS_TYPE_INT64, &utc,
396                                     DBUS_TYPE_BOOLEAN, &relative,
397                                     DBUS_TYPE_BOOLEAN, &interactive,
398                                     DBUS_TYPE_INVALID))
399                         return bus_send_error_reply(connection, message, &error, -EINVAL);
400
401                 if (!relative && utc <= 0)
402                         return bus_send_error_reply(connection, message, NULL, -EINVAL);
403
404                 if (!relative || utc != 0) {
405                         struct timespec ts;
406
407                         r = verify_polkit(connection, message, "org.freedesktop.timedate1.set-time", interactive, &error);
408                         if (r < 0)
409                                 return bus_send_error_reply(connection, message, &error, r);
410
411                         if (relative)
412                                 timespec_store(&ts, now(CLOCK_REALTIME) + utc);
413                         else
414                                 timespec_store(&ts, utc);
415
416                         if (clock_settime(CLOCK_REALTIME, &ts) < 0) {
417                                 log_error("Failed to set local time: %m");
418                                 return bus_send_error_reply(connection, message, NULL, -errno);
419                         }
420
421                         log_info("Changed local time to %s", ctime(&ts.tv_sec));
422                 }
423
424         } else
425                 return bus_default_message_handler(connection, message, INTROSPECTION, INTERFACES_LIST, properties);
426
427         if (!(reply = dbus_message_new_method_return(message)))
428                 goto oom;
429
430         if (!dbus_connection_send(connection, reply, NULL))
431                 goto oom;
432
433         dbus_message_unref(reply);
434         reply = NULL;
435
436         if (changed) {
437
438                 if (!dbus_connection_send(connection, changed, NULL))
439                         goto oom;
440
441                 dbus_message_unref(changed);
442         }
443
444         return DBUS_HANDLER_RESULT_HANDLED;
445
446 oom:
447         if (reply)
448                 dbus_message_unref(reply);
449
450         if (changed)
451                 dbus_message_unref(changed);
452
453         dbus_error_free(&error);
454
455         return DBUS_HANDLER_RESULT_NEED_MEMORY;
456 }
457
458 static int connect_bus(DBusConnection **_bus) {
459         static const DBusObjectPathVTable timedate_vtable = {
460                 .message_function = timedate_message_handler
461         };
462         DBusError error;
463         DBusConnection *bus = NULL;
464         int r;
465
466         assert(_bus);
467
468         dbus_error_init(&error);
469
470         bus = dbus_bus_get_private(DBUS_BUS_SYSTEM, &error);
471         if (!bus) {
472                 log_error("Failed to get system D-Bus connection: %s", error.message);
473                 r = -ECONNREFUSED;
474                 goto fail;
475         }
476
477         if (!dbus_connection_register_object_path(bus, "/org/freedesktop/timedate1", &timedate_vtable, NULL)) {
478                 log_error("Not enough memory");
479                 r = -ENOMEM;
480                 goto fail;
481         }
482
483         if (dbus_bus_request_name(bus, "org.freedesktop.timedate1", DBUS_NAME_FLAG_DO_NOT_QUEUE, &error) < 0) {
484                 log_error("Failed to register name on bus: %s", error.message);
485                 r = -EEXIST;
486                 goto fail;
487         }
488
489         if (_bus)
490                 *_bus = bus;
491
492         return 0;
493
494 fail:
495         dbus_connection_close(bus);
496         dbus_connection_unref(bus);
497
498         dbus_error_free(&error);
499
500         return r;
501 }
502
503 int main(int argc, char *argv[]) {
504         int r;
505         DBusConnection *bus = NULL;
506
507         log_set_target(LOG_TARGET_AUTO);
508         log_parse_environment();
509         log_open();
510
511         if (argc != 1) {
512                 log_error("This program takes no arguments.");
513                 r = -EINVAL;
514                 goto finish;
515         }
516
517         umask(0022);
518
519         r = read_data();
520         if (r < 0) {
521                 log_error("Failed to read timezone data: %s", strerror(-r));
522                 goto finish;
523         }
524
525         r = connect_bus(&bus);
526         if (r < 0)
527                 goto finish;
528
529         while (dbus_connection_read_write_dispatch(bus, -1))
530                 ;
531
532         r = 0;
533
534 finish:
535         free_data();
536
537         if (bus) {
538                 dbus_connection_flush(bus);
539                 dbus_connection_close(bus);
540                 dbus_connection_unref(bus);
541         }
542
543         return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
544 }