/* * kissdx - KiSS PC-Link Daemon eXtended (based on kissd) * * Copyright (C) 2005 Stelian Pop * Portions Copyright (C) 2006 Vidar Tysse * * Heavily based on kiss4lin, * Copyright (C) 2004 Jacob Kolding * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * 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., 675 Mass Ave, Cambridge, MA 02139, USA. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kissdx.h" #include "utils.h" void global_param_init(void); void config_file_read(void); char *c_opt = NULL; int a_opt = 0, d_opt = 0, k_opt = 0, v_opt = 0; char audiopath[PATH_MAX] = ""; char videopath[PATH_MAX] = ""; char picturepath[PATH_MAX] = ""; char persistentstoragepath[PATH_MAX] = ""; char kmlurl[PATH_MAX] = ""; char pretrigger[PATH_MAX] = ""; char posttrigger[PATH_MAX] = ""; char audiofileextensions[255] = ""; char videofileextensions[255] = ""; char picturefileextensions[255] = ""; char isofileextensions[255] = "iso"; int max_recent_files = 30; char server_character_set[64] = ""; char client_character_set[64] = ""; char subtitle_catchall_pattern[PATH_MAX] = ""; int picturetargetwidth = 0; int picturetargetheight = 0; int picturemaxzoompercent = 100; int picturecachesize = 0; int picturecachetrimminginterval = 5; int networktimeoutinterval = 0; #define PORT_PCLINK 8000 #define PORT_KML 8888 // Global constants int standard_filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; int standard_dir_filemode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; // Global flag int is_socket_timed_out = 0; char * extended_logfile_format(const char *format) { static char extended_format[1024]; if (v_opt) { char timestr[32]; time_t current_time = time(NULL); strftime(timestr, sizeof timestr, "%F %T", localtime(¤t_time)); snprintf(extended_format, sizeof extended_format, "[%5d] %s %s", getpid(), timestr, format); } else { strncpy(extended_format, format, sizeof extended_format); } return extended_format; } static void server_shutdown(int signal) { log("%s", "shutting down kissdx..."); exit(0); } static void kill_children(int signal) { int pid, preserved_errno = errno; do { do { errno = 0; pid = waitpid(-1, NULL, WNOHANG); } while (pid <= 0 && errno == EINTR); if (pid > 0) logv("Child process [%5d] exited", pid); } while (pid > 0); errno = preserved_errno; } int get_recent_path(char *recentpath, const char *mediapath) { char recent[PATH_MAX]; int len; if (!strncmp(mediapath, audiopath, strlen(audiopath)) || (*persistentstoragepath && strstr(mediapath, "/audio/.recent/"))) { if (*persistentstoragepath) len = snprintf(recent, sizeof recent, "%s/audio/.recent", persistentstoragepath); else len = snprintf(recent, sizeof recent, "%s/.recent", audiopath); } else if (!strncmp(mediapath, videopath, strlen(videopath)) || (*persistentstoragepath && strstr(mediapath, "/video/.recent/"))) { if (*persistentstoragepath) len = snprintf(recent, sizeof recent, "%s/video/.recent", persistentstoragepath); else len = snprintf(recent, sizeof recent, "%s/.recent", videopath); } else if (!strncmp(mediapath, picturepath, strlen(picturepath)) || (*persistentstoragepath && strstr(mediapath, "/picture/.recent/"))) { if (*persistentstoragepath) len = snprintf(recent, sizeof recent, "%s/picture/.recent", persistentstoragepath); else len = snprintf(recent, sizeof recent, "%s/.recent", picturepath); } else { log ("get_recent_path: Unknown media path in \"%s\"", mediapath); return -1; } if (len >= sizeof recent) { log ("get_recent_path: Buffer too small for filename \"%s\"", mediapath); return -1; } strcpy(recentpath, recent); return 0; } static int stat_outdated_path(const char *dir, const char *name) { char fullpath[PATH_MAX]; struct stat statbuf; strcpy(fullpath, dir); strncat(fullpath, "/", sizeof fullpath); strncat(fullpath, name, sizeof fullpath); if (stat(fullpath, &statbuf) == 0) { log("Old directory must be moved to new location: %s", fullpath); return 1; } return 0; } static int parse_config(void) { FILE *f; char *home, *configpath; char buffer[512]; int is_config_error = 0; // Set all config options to their default values *audiopath = '\0'; *videopath = '\0'; *picturepath = '\0'; *persistentstoragepath = '\0'; *kmlurl = '\0'; *pretrigger = '\0'; *posttrigger = '\0'; *audiofileextensions = '\0'; *videofileextensions = '\0'; *picturefileextensions = '\0'; strcpy(isofileextensions, "iso"); max_recent_files = 30; *server_character_set = '\0'; *client_character_set = '\0'; *subtitle_catchall_pattern = '\0'; picturetargetwidth = 0; picturetargetheight = 0; picturemaxzoompercent = 100; picturecachesize = 0; picturecachetrimminginterval = 5; networktimeoutinterval = 0; configpath = c_opt; if (configpath != NULL) { if ((f = fopen(configpath, "r")) == NULL) { log("unable to open: %s", configpath); return 1; } } else { if ((home = getenv("HOME")) == NULL) home = "/"; snprintf(buffer, sizeof(buffer), "%s/.kissdx.conf", home); configpath = buffer; if ((f = fopen(configpath, "r")) == NULL) { snprintf(buffer, sizeof(buffer), "%s/.kissd.conf", home); configpath = buffer; if ((f = fopen(configpath, "r")) == NULL) { configpath = "/etc/kissdx.conf"; if ((f = fopen(configpath, "r")) == NULL) { configpath = "/etc/kissd.conf"; if ((f = fopen(configpath, "r")) == NULL) { log0("config file not found!"); log0("searched $HOME/ and /etc/ for kissdx.conf and kissd.conf"); return 1; } } } } } if (v_opt) log("Using config file %s", configpath); while (fgets(buffer, sizeof(buffer), f) != NULL) { char option[512], value[512]; char *b = buffer; int rel_path = 0; for(; *b == ' ' || *b == '\t' || *b == '\n'; b++); if (b[0] == '#') continue; if (strlen(b) == 0) continue; if (sscanf(b, " %s = %s ", option, value) != 2) { log("invalid line in config file: %s", buffer); return 1; } if (!strcasecmp(option, "audiopath")) { strncpy(audiopath, value, sizeof(audiopath)); audiopath[sizeof(audiopath) - 1] = '\0'; rel_path = (value[0] != '/'); } else if (!strcasecmp(option, "videopath")) { strncpy(videopath, value, sizeof(videopath)); audiopath[sizeof(audiopath) - 1] = '\0'; rel_path = (value[0] != '/'); } else if (!strcasecmp(option, "picturepath")) { strncpy(picturepath, value, sizeof(picturepath)); picturepath[sizeof(picturepath) - 1] = '\0'; rel_path = (value[0] != '/'); } else if (!strcasecmp(option, "persistentstoragepath")) { strncpy(persistentstoragepath, value, sizeof(persistentstoragepath)); persistentstoragepath[sizeof(persistentstoragepath) - 1] = '\0'; rel_path = (value[0] != '/'); } else if (!strcasecmp(option, "kmlurl")) { strncpy(kmlurl, value, sizeof(kmlurl)); kmlurl[sizeof(kmlurl) - 1] = '\0'; } else if (!strcasecmp(option, "pretrigger")) { strncpy(pretrigger, value, sizeof(pretrigger)); pretrigger[sizeof(pretrigger) - 1] = '\0'; rel_path = (value[0] != '/'); } else if (!strcasecmp(option, "posttrigger")) { strncpy(posttrigger, value, sizeof(posttrigger)); posttrigger[sizeof(posttrigger) - 1] = '\0'; rel_path = (value[0] != '/'); } else if (!strcasecmp(option, "audiofileextensions")) { strncpy(audiofileextensions, value, sizeof(audiofileextensions)); audiofileextensions[sizeof(audiofileextensions) - 1] = '\0'; make_lowercase(audiofileextensions); } else if (!strcasecmp(option, "videofileextensions")) { strncpy(videofileextensions, value, sizeof(videofileextensions)); videofileextensions[sizeof(videofileextensions) - 1] = '\0'; make_lowercase(videofileextensions); } else if (!strcasecmp(option, "picturefileextensions")) { strncpy(picturefileextensions, value, sizeof(picturefileextensions)); picturefileextensions[sizeof(picturefileextensions) - 1] = '\0'; make_lowercase(picturefileextensions); } else if (!strcasecmp(option, "isofileextensions")) { strncpy(isofileextensions, value, sizeof(isofileextensions)); isofileextensions[sizeof(isofileextensions) - 1] = '\0'; make_lowercase(isofileextensions); } else if (!strcasecmp(option, "max_recent_files")) { max_recent_files = abs(atoi(value)); } else if (!strcasecmp(option, "server_character_set")) { strncpy(server_character_set, value, sizeof(server_character_set)); server_character_set[sizeof(server_character_set) - 1] = '\0'; } else if (!strcasecmp(option, "client_character_set")) { strncpy(client_character_set, value, sizeof(client_character_set)); client_character_set[sizeof(client_character_set) - 1] = '\0'; } else if (!strcasecmp(option, "subtitlefilemapping")) { if (!strncmp(value, "*:", 2) && strlen(value) > 2) { strncpy(subtitle_catchall_pattern, value + 2, sizeof(subtitle_catchall_pattern)); subtitle_catchall_pattern[sizeof(subtitle_catchall_pattern) - 1] = '\0'; } else { log("Error in %s option in config file: %s", option, "Must start with '*:' followed by a filename pattern."); is_config_error = 1; } } else if (!strcasecmp(option, "picturetargetwidth")) { picturetargetwidth = abs(atoi(value)); } else if (!strcasecmp(option, "picturetargetheight")) { picturetargetheight = abs(atoi(value)); } else if (!strcasecmp(option, "picturemaxzoompercent")) { picturemaxzoompercent = abs(atoi(value)); } else if (!strcasecmp(option, "picturecachesize")) { picturecachesize = atoi(value); } else if (!strcasecmp(option, "picturecachetrimminginterval")) { picturecachetrimminginterval = abs(atoi(value)); } else if (!strcasecmp(option, "networktimeoutinterval")) { networktimeoutinterval = max(atoi(value), 0); } else { log("unknown option in config file: %s", option); is_config_error = 1; } if (rel_path) { log("paths must be absolute refs in config file: %s", value); is_config_error = 1; } } if (audiopath[0] == '\0') { log0("audiopath not set in config file"); is_config_error = 1; } if (videopath[0] == '\0') { log0("videopath not set in config file"); is_config_error = 1; } if (picturepath[0] == '\0') { log0("picturepath not set in config file"); is_config_error = 1; } clean_pathname(audiopath); clean_pathname(videopath); clean_pathname(picturepath); if (persistentstoragepath[0] != '\0' && !is_config_error) { // Look for .recent directories inside media directories. If they are still there, it's a config error. int is_recent_there = 0; clean_pathname(persistentstoragepath); if (stat_outdated_path(audiopath, ".recent")) is_recent_there = 1; if (stat_outdated_path(videopath, ".recent")) is_recent_there = 1; if (stat_outdated_path(picturepath, ".recent")) is_recent_there = 1; if (is_recent_there) { log("kissdx will not start until the .recent directories have been deleted or moved to subdirectories of %s", persistentstoragepath); is_config_error = 1; } if (stat_outdated_path(picturepath, ".piccache")) { log("kissdx will not start until the .piccache directory has been deleted or moved to %s", persistentstoragepath); is_config_error = 1; } // Create the .recent directories now to aid user in moving stuff (and to aid ourselves later...) char path[PATH_MAX]; if (mkdir(persistentstoragepath, standard_dir_filemode) >= 0) chmod(persistentstoragepath, standard_dir_filemode); strcpy(path, persistentstoragepath); strncat(path, "/audio", sizeof path); if (mkdir(path, standard_dir_filemode) >= 0) chmod(path, standard_dir_filemode); strncat(path, "/.recent", sizeof path); if (mkdir(path, standard_dir_filemode) >= 0) chmod(path, standard_dir_filemode); strcpy(path, persistentstoragepath); strncat(path, "/video", sizeof path); if (mkdir(path, standard_dir_filemode) >= 0) chmod(path, standard_dir_filemode); strncat(path, "/.recent", sizeof path); if (mkdir(path, standard_dir_filemode) >= 0) chmod(path, standard_dir_filemode); strcpy(path, persistentstoragepath); strncat(path, "/picture", sizeof path); if (mkdir(path, standard_dir_filemode) >= 0) chmod(path, standard_dir_filemode); strncat(path, "/.recent", sizeof path); if (mkdir(path, standard_dir_filemode) >= 0) chmod(path, standard_dir_filemode); } if (k_opt && kmlurl[0] == '\0') { log0("kmlurl not set in config file"); is_config_error = 1; } if (is_config_error) { log0("Exiting due to configuration error(s)."); return 1; } fclose(f); if (v_opt) { log("audiopath = %s", audiopath); log("videopath = %s", videopath); log("picturepath = %s", picturepath); log("persistentstoragepath = %s", persistentstoragepath); log("kmlurl = %s", kmlurl); log("pretrigger = %s", pretrigger); log("posttrigger = %s", posttrigger); log("audiofileextensions = %s", audiofileextensions); log("videofileextensions = %s", videofileextensions); log("picturefileextensions = %s", picturefileextensions); log("isofileextensions = %s", isofileextensions); log("max_recent_files = %d", max_recent_files); log("server_character_set = %s", server_character_set); log("client_character_set = %s", client_character_set); log("subtitle_catchall_pattern = %s", subtitle_catchall_pattern); log("picturetargetwidth = %d", picturetargetwidth); log("picturetargetheight = %d", picturetargetheight); log("picturemaxzoompercent = %d", picturemaxzoompercent); log("picturecachesize = %d", picturecachesize); log("picturecachetrimminginterval = %d", picturecachetrimminginterval); log("networktimeoutinterval = %d", networktimeoutinterval); } return 0; } static void create_socket(int *sock, int port, int type) { struct sockaddr_in sin; int yes = 1; /* get an internet domain socket */ if ((*sock = socket(AF_INET, type, 0)) < 0) { log("socket: %s", strerror(errno)); exit(1); } /* lose the pesky "address already in use" error message */ if (setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) < 0) { log("setsockopt: %s", strerror(errno)); exit(1); } /* complete the socket structure */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port); /* bind the socket to the port number */ if (bind(*sock, (struct sockaddr *) &sin, sizeof(sin)) < 0) { log("bind: %s", strerror(errno)); exit(1); } /* everything done for a udp socket */ if (type == SOCK_DGRAM) { return; } /* show that we are willing to listen */ if (listen(*sock, 5) < 0) { log("listen: %s", strerror(errno)); exit(1); } } static void udp_responder (int sd) { struct sockaddr_in sin; socklen_t sinlen = sizeof(sin); char buf[128]; /* complete the socket structure */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(PORT_PCLINK); if (recvfrom(sd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &sinlen) < 0) { log("recvfrom: %s", strerror(errno)); return; // exit gracefully } if (strstr(buf, "ARE_YOU_KISS_PCLINK_SERVER?")) { char hostname[255]; logv("Received identity request from %s", inet_ntoa(sin.sin_addr)); // Re-read config file to get at any changes. Exit if config error. if (parse_config()) exit(1); if (gethostname(hostname, sizeof(hostname)) < 0) { log("gethostname: %s", strerror(errno)); return; // exit gracefully } strcat(hostname, " running kissdx-"); strcat(hostname, KISSD_VERSION); if (sendto(sd, hostname, strlen(hostname), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0) { log("sendto: %s", strerror(errno)); } } } static void do_daemon(void) { int sd_server, sd_client; int sd_pclink, sd_kml = -1; int sd_udp_responder; struct sockaddr_in pin; socklen_t addrlen; fd_set set; signal(SIGINT, server_shutdown); signal(SIGCHLD, kill_children); if (d_opt) openlog("kissdx", LOG_PID, LOG_DAEMON); log("%s", "starting kissdx..."); create_socket(&sd_pclink, PORT_PCLINK, SOCK_STREAM); create_socket(&sd_udp_responder, PORT_PCLINK, SOCK_DGRAM); if (k_opt) create_socket(&sd_kml, PORT_KML, SOCK_STREAM); again: FD_ZERO(&set); FD_SET(sd_pclink, &set); FD_SET(sd_udp_responder, &set); if (k_opt) FD_SET(sd_kml, &set); memset(&pin, 0, sizeof(pin)); memset(&addrlen, 0, sizeof(addrlen)); if (select(max(max(sd_pclink, sd_kml), sd_udp_responder) + 1, &set, NULL, NULL, NULL) < 0) { if (errno == EINTR) goto again; log("select: %s", strerror(errno)); exit(1); } if (FD_ISSET(sd_udp_responder, &set)) { udp_responder(sd_udp_responder); goto again; } if (FD_ISSET(sd_pclink, &set)) sd_server = sd_pclink; else sd_server = sd_kml; if ((sd_client = accept(sd_server, (struct sockaddr *)&pin, &addrlen)) < 0) { log("accept: %s", strerror(errno)); exit(1); } switch (fork()) { case -1: log("fork: %s", strerror(errno)); exit(1); case 0: close(sd_server); logv("%s", "KiSS started connection"); if (FD_ISSET(sd_pclink, &set)) handle_request(sd_client); else handle_kmlrequest(sd_client); if (is_socket_timed_out) shutdown(sd_client, SHUT_RDWR); close(sd_client); logv("%s closed connection", is_socket_timed_out? "kissdx shut down and" : "KiSS"); _exit(0); default: close(sd_client); break; } goto again; /* not reached */ } static void usage(const char *program) { fprintf(stderr, "KiSS PC-Link daemon " KISSD_VERSION "\n\n" "Usage: %s [OPTION]\n" "\nOptions:\n" "\t-a, --all\t\tdo not hide entries starting with .\n" "\t-c, --config=FILE\tpath to the configuration file\n" "\t-d, --daemon\t\tfork into background and log using syslog\n" "\t-h, --help\t\tprint this on-line help\n" "\t-k, --kml\t\tforward KML requests to another URL\n" "\t-v, --verbose\t\tlog every request and response\n", program); } int main(int argc, char *argv[]) { int c; static struct option long_options[] = { { "all", 0, NULL, 'a' }, { "config", 1, NULL, 'c' }, { "daemon", 0, NULL, 'd' }, { "help", 0, NULL, 'h' }, { "kml", 0, NULL, 'k' }, { "verbose", 0, NULL, 'v' }, { NULL, 0, NULL, 0 } }; setlocale (LC_ALL, ""); while (1) { c = getopt_long(argc, argv, "ac:dhkv", long_options, NULL); if (c == -1) break; switch (c) { case 'a': a_opt = 1; break; case 'c': c_opt = optarg; break; case 'd': d_opt = 1; break; case 'k': k_opt = 1; break; case 'v': v_opt = 1; break; default: usage(argv[0]); exit(1); break; } } if (parse_config()) exit(1); if (d_opt) { int fd; switch(fork()) { case 0: break; case -1: perror("fork"); exit(1); default: _exit(0); } if (setsid() < 0) { perror("setsid"); } switch(fork()) { case 0: break; case -1: perror("fork 2"); exit(1); default: _exit(0); } chdir("/"); // Avoid tying up access to whatever dir we were run from close(0); close(1); close(2); if ((fd = open("/dev/null", O_RDONLY)) != 0) { dup2(fd, 0); close(fd); } if ((fd = open("/dev/null", O_WRONLY)) != 1) { dup2(fd, 1); close(fd); } if ((fd = open("/dev/null", O_WRONLY)) != 2) { dup2(fd, 2); close(fd); } do_daemon(); } else do_daemon(); exit(0); }