blob: 89c448954f89b2b00d38253df6c509ed09b7c840 [file] [log] [blame]
/*
*
* Connection Manager
*
* Copyright (C) 2007-2012 Intel Corporation. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/inotify.h>
#include <glib.h>
#include "connman.h"
#define ETC_SYSCONFIG_CLOCK "/etc/sysconfig/clock"
#define USR_SHARE_ZONEINFO "/usr/share/zoneinfo"
#define USR_SHARE_ZONEINFO_MAP USR_SHARE_ZONEINFO "/zone1970.tab"
#define USR_SHARE_ZONEINFO_MAP_OLD USR_SHARE_ZONEINFO "/zone.tab"
static char *read_key_file(const char *pathname, const char *key)
{
struct stat st;
char *map, *ptr, *str;
off_t ptrlen, keylen;
int fd;
fd = open(pathname, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return NULL;
if (fstat(fd, &st) < 0) {
close(fd);
return NULL;
}
map = mmap(0, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (!map || map == MAP_FAILED) {
close(fd);
return NULL;
}
ptr = map;
ptrlen = st.st_size;
keylen = strlen(key);
while (ptrlen > keylen + 1) {
int cmp = strncmp(ptr, key, keylen);
if (cmp == 0) {
if (ptr == map)
break;
if (*(ptr - 1) == '\n' && *(ptr + keylen) == '=')
break;
}
ptr = memchr(ptr + 1, key[0], ptrlen - 1);
if (!ptr)
break;
ptrlen = st.st_size - (ptr - map);
}
if (ptr) {
char *end, *val;
ptrlen = st.st_size - (ptr - map);
end = memchr(ptr, '\n', ptrlen);
if (end)
ptrlen = end - ptr;
val = memchr(ptr, '"', ptrlen);
if (val) {
end = memchr(val + 1, '"', end - val - 1);
if (end)
str = g_strndup(val + 1, end - val - 1);
else
str = NULL;
} else
str = g_strndup(ptr + keylen + 1, ptrlen - keylen - 1);
} else
str = NULL;
munmap(map, st.st_size);
close(fd);
return str;
}
static int compare_file(void *src_map, struct stat *src_st,
const char *real_path, const char *pathname)
{
struct stat dst_st;
void *dst_map;
int fd, result;
DBG("real path %s path name %s", real_path, pathname);
if (real_path && g_strcmp0(real_path, pathname))
return -1;
fd = open(pathname, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return -1;
if (fstat(fd, &dst_st) < 0) {
close(fd);
return -1;
}
if (src_st->st_size != dst_st.st_size) {
close(fd);
return -1;
}
dst_map = mmap(0, dst_st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (!dst_map || dst_map == MAP_FAILED) {
close(fd);
return -1;
}
result = memcmp(src_map, dst_map, src_st->st_size);
munmap(dst_map, dst_st.st_size);
close(fd);
return result;
}
static char *find_origin(void *src_map, struct stat *src_st,
const char* real_path, const char *basepath,
const char *subpath)
{
DIR *dir;
struct dirent *d;
char *str, pathname[PATH_MAX];
struct stat buf;
int ret;
if (!subpath)
strncpy(pathname, basepath, sizeof(pathname) - 1);
else
snprintf(pathname, sizeof(pathname),
"%s/%s", basepath, subpath);
dir = opendir(pathname);
if (!dir)
return NULL;
while ((d = readdir(dir))) {
if (strcmp(d->d_name, ".") == 0 ||
strcmp(d->d_name, "..") == 0 ||
strcmp(d->d_name, "posix") == 0 ||
strcmp(d->d_name, "right") == 0)
continue;
switch (d->d_type) {
case DT_REG:
if (!subpath)
snprintf(pathname, PATH_MAX,
"%s/%s", basepath, d->d_name);
else
snprintf(pathname, PATH_MAX,
"%s/%s/%s", basepath,
subpath, d->d_name);
if (compare_file(src_map, src_st, real_path, pathname)
== 0) {
if (!subpath)
str = g_strdup(d->d_name);
else
str = g_strdup_printf("%s/%s",
subpath, d->d_name);
closedir(dir);
return str;
}
break;
case DT_UNKNOWN:
/*
* If there is no d_type support use fstatat()
* to check if d_name is directory
*/
ret = fstatat(dirfd(dir), d->d_name, &buf, 0);
if (ret < 0)
continue;
if ((buf.st_mode & S_IFDIR) == 0)
continue;
/* fall through */
case DT_DIR:
if (!subpath)
strncpy(pathname, d->d_name, sizeof(pathname));
else
snprintf(pathname, sizeof(pathname),
"%s/%s", subpath, d->d_name);
str = find_origin(src_map, src_st, real_path, basepath,
pathname);
if (str) {
closedir(dir);
return str;
}
break;
}
}
closedir(dir);
return NULL;
}
/* TZ map file format: Countrycodes Coordinates TZ Comments */
enum tz_map_item {
TZ_MAP_ITEM_ISO3166 = 0,
TZ_MAP_ITEM_COORDINATE = 1,
TZ_MAP_ITEM_TIMEZONE = 2,
TZ_MAP_ITEM_COMMENT = 3,
};
static bool iso3166_matches(const char *codes, const char *iso3166)
{
gchar **tokens;
guint len;
guint i;
bool ret = false;
if (!codes || !iso3166)
return false;
tokens = g_strsplit(codes, ",", -1);
if (!tokens)
return false;
len = g_strv_length(tokens);
if (len < 2) {
g_strfreev(tokens);
return false;
}
DBG("search %s from %s", iso3166, codes);
for (i = 0; i < len; i++) {
DBG("#%d %s", i, tokens[i]);
if (!g_strcmp0(tokens[i], iso3166)) {
ret = true;
break;
}
}
g_strfreev(tokens);
return ret;
}
static char *get_timezone_alpha2(const char *zone, const char *mapfile,
enum tz_map_item map_item)
{
GIOChannel *channel;
struct stat st;
char **tokens;
char *line;
char *alpha2 = NULL;
gsize len;
bool found = false;
int fd;
if (!zone)
return NULL;
fd = open(mapfile, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
connman_warn("failed to open zoneinfo map %s", mapfile);
return NULL;
}
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
connman_warn("zoneinfo map does not exist/not regular file");
close(fd);
return NULL;
}
channel = g_io_channel_unix_new(fd);
if (!channel) {
connman_warn("failed to create io channel for %s", mapfile);
close(fd);
return NULL;
}
DBG("read %s for %s", mapfile, zone);
g_io_channel_set_encoding(channel, "UTF-8", NULL);
while (g_io_channel_read_line(channel, &line, &len, NULL, NULL) ==
G_IO_STATUS_NORMAL) {
found = false;
if (!line || !*line || *line == '#' || *line == '\n') {
g_free(line);
continue;
}
tokens = g_strsplit_set(line, " \t", 4);
if (!tokens) {
connman_warn("line %s failed to parse", line);
g_free(line);
continue;
}
if (g_strv_length(tokens) >= 3) {
switch (map_item) {
case TZ_MAP_ITEM_ISO3166:
if (!g_strcmp0(tokens[0], zone)) {
found = true;
break;
}
if (iso3166_matches(tokens[0], zone))
found = true;
break;
case TZ_MAP_ITEM_COORDINATE:
break;
case TZ_MAP_ITEM_TIMEZONE:
if (!g_strcmp0(g_strstrip(tokens[2]), zone))
found = true;
break;
case TZ_MAP_ITEM_COMMENT:
break;
}
/*
* Multiple country codes can be listed, use the first
* 2 chars as backends such as gsupplicant support only
* the main country code.
*/
if (found)
alpha2 = g_strndup(g_strstrip(tokens[0]), 2);
}
g_strfreev(tokens);
g_free(line);
if (alpha2) {
if (strlen(alpha2) != 2) {
connman_warn("Invalid ISO3166 code %s", alpha2);
g_free(alpha2);
alpha2 = NULL;
} else {
DBG("Zone %s ISO3166 country code %s", zone,
alpha2);
break;
}
}
}
g_io_channel_unref(channel);
close(fd);
return alpha2;
}
static char *try_get_timezone_alpha2(const char *zone)
{
char *alpha2;
char *alpha2_old;
/* First try the official map */
alpha2 = get_timezone_alpha2(zone, USR_SHARE_ZONEINFO_MAP,
TZ_MAP_ITEM_TIMEZONE);
if (alpha2)
return alpha2;
DBG("%s not found in %s", zone, USR_SHARE_ZONEINFO_MAP);
/* The zone was not found in official map, try with deprecated */
alpha2_old = get_timezone_alpha2(zone, USR_SHARE_ZONEINFO_MAP_OLD,
TZ_MAP_ITEM_TIMEZONE);
if (!alpha2_old) {
DBG("%s not found in %s", zone, USR_SHARE_ZONEINFO_MAP_OLD);
return NULL;
}
/*
* Found from deprecated, try to get main region code from official new
* map using the iso3166 search. This is because some of the codes
* defined in the deprecated are not supported by the backends, e.g.,
* gsupplicant and the main code should be used.
*/
alpha2 = get_timezone_alpha2(alpha2_old, USR_SHARE_ZONEINFO_MAP,
TZ_MAP_ITEM_ISO3166);
DBG("%s -> ISO3166 %s found in %s as %s", zone, alpha2_old,
USR_SHARE_ZONEINFO_MAP, alpha2);
g_free(alpha2_old);
return alpha2;
}
char *__connman_timezone_lookup(void)
{
struct stat st;
void *map;
int fd;
char *zone;
char *alpha2;
const char *local_time;
char real_path[PATH_MAX];
zone = read_key_file(ETC_SYSCONFIG_CLOCK, "ZONE");
DBG("sysconfig zone %s", zone);
local_time = connman_setting_get_string("Localtime");
fd = open(local_time, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
g_free(zone);
return NULL;
}
if (fstat(fd, &st) < 0)
goto done;
if (!realpath(local_time, real_path)) {
connman_error("Failed to get real path of %s: %d/%s",
local_time, errno, strerror(errno));
g_free(zone);
close(fd);
return NULL;
}
if (S_ISREG(st.st_mode)) {
map = mmap(0, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (!map || map == MAP_FAILED) {
g_free(zone);
zone = NULL;
goto done;
}
if (zone) {
char pathname[PATH_MAX];
snprintf(pathname, PATH_MAX, "%s/%s",
USR_SHARE_ZONEINFO, zone);
if (compare_file(map, &st, NULL, pathname) != 0) {
g_free(zone);
zone = NULL;
}
}
if (!zone)
zone = find_origin(map, &st, real_path,
USR_SHARE_ZONEINFO, NULL);
munmap(map, st.st_size);
} else {
g_free(zone);
zone = NULL;
}
done:
close(fd);
DBG("localtime zone %s", zone);
if (connman_setting_get_bool("RegdomFollowsTimezone")) {
alpha2 = try_get_timezone_alpha2(zone);
if (alpha2) {
DBG("change regdom to %s", alpha2);
connman_technology_set_regdom(alpha2);
g_free(alpha2);
}
}
return zone;
}
static int write_file(void *src_map, struct stat *src_st, const char *pathname)
{
struct stat st;
int fd;
ssize_t written;
DBG("pathname %s", pathname);
if (lstat(pathname, &st) == 0) {
if (S_ISLNK(st.st_mode))
unlink(pathname);
}
fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0)
return -EIO;
written = write(fd, src_map, src_st->st_size);
close(fd);
if (written < 0)
return -EIO;
return 0;
}
int __connman_timezone_change(const char *zone)
{
struct stat st;
char *map, pathname[PATH_MAX];
int fd, err;
DBG("zone %s", zone);
snprintf(pathname, PATH_MAX, "%s/%s", USR_SHARE_ZONEINFO, zone);
fd = open(pathname, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return -EINVAL;
if (fstat(fd, &st) < 0) {
close(fd);
return -EIO;
}
map = mmap(0, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (!map || map == MAP_FAILED) {
close(fd);
return -EIO;
}
err = write_file(map, &st, connman_setting_get_string("Localtime"));
munmap(map, st.st_size);
close(fd);
return err;
}
static guint inotify_watch = 0;
static gboolean inotify_data(GIOChannel *channel, GIOCondition cond,
gpointer user_data)
{
char buffer[256];
void *ptr = buffer;
GIOStatus status;
gsize bytes_read;
DBG("");
if (cond & (G_IO_NVAL | G_IO_ERR | G_IO_HUP)) {
inotify_watch = 0;
return FALSE;
}
status = g_io_channel_read_chars(channel, buffer, sizeof(buffer),
&bytes_read, NULL);
switch (status) {
case G_IO_STATUS_NORMAL:
break;
case G_IO_STATUS_AGAIN:
return TRUE;
default:
inotify_watch = 0;
return FALSE;
}
DBG("bytes read %zd", bytes_read);
while (bytes_read > 0) {
struct inotify_event *event = ptr;
if (bytes_read < sizeof(*event))
break;
ptr += sizeof(*event);
bytes_read -= sizeof(*event);
if (event->len == 0)
continue;
if (bytes_read < event->len)
break;
ptr += event->len;
bytes_read -= event->len;
if (g_strcmp0(event->name, "localtime") == 0)
__connman_clock_update_timezone();
}
return TRUE;
}
int __connman_timezone_init(void)
{
GIOChannel *channel;
char *dirname;
int fd, wd;
DBG("");
fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (fd < 0)
return -EIO;
channel = g_io_channel_unix_new(fd);
if (!channel) {
close(fd);
return -EIO;
}
g_io_channel_set_close_on_unref(channel, TRUE);
g_io_channel_set_encoding(channel, NULL, NULL);
g_io_channel_set_buffered(channel, FALSE);
inotify_watch = g_io_add_watch(channel,
G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR,
inotify_data, NULL);
g_io_channel_unref(channel);
dirname = g_path_get_dirname(connman_setting_get_string("Localtime"));
wd = inotify_add_watch(fd, dirname, IN_CREATE | IN_DONT_FOLLOW |
IN_CLOSE_WRITE | IN_MOVED_TO);
g_free(dirname);
if (wd < 0)
return -EIO;
return 0;
}
void __connman_timezone_cleanup(void)
{
DBG("");
if (inotify_watch > 0) {
g_source_remove(inotify_watch);
inotify_watch = 0;
}
}