/* * svdrp.c: Simple Video Disk Recorder Protocol * * See the main source file 'vdr.c' for copyright information and * how to reach the author. * * The "Simple Video Disk Recorder Protocol" (SVDRP) was inspired * by the "Simple Mail Transfer Protocol" (SMTP) and is fully ASCII * text based. Therefore you can simply 'telnet' to your VDR port * and interact with the Video Disk Recorder - or write a full featured * graphical interface that sits on top of an SVDRP connection. * * $Id: svdrp.c 4.2 2015/05/22 11:01:33 kls Exp $ */ #include "svdrp.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "channels.h" #include "config.h" #include "device.h" #include "eitscan.h" #include "keys.h" #include "menu.h" #include "plugin.h" #include "recording.h" #include "remote.h" #include "skins.h" #include "thread.h" #include "timers.h" #include "videodir.h" static bool DumpSVDRPDataTransfer = false; #define dbgsvdrp(a...) if (DumpSVDRPDataTransfer) fprintf(stderr, a) static const char *HostName(void) { static char buffer[HOST_NAME_MAX] = ""; if (!*buffer) { if (gethostname(buffer, sizeof(buffer)) < 0) { LOG_ERROR; strcpy(buffer, "vdr"); } } return buffer; } // --- cIpAddress ------------------------------------------------------------ class cIpAddress { private: cString address; int port; cString connection; public: cIpAddress(void); cIpAddress(const char *Address, int Port); const char *Address(void) const { return address; } int Port(void) const { return port; } void Set(const char *Address, int Port); void Set(const sockaddr *SockAddr); const char *Connection(void) const { return connection; } }; cIpAddress::cIpAddress(void) { Set(INADDR_ANY, 0); } cIpAddress::cIpAddress(const char *Address, int Port) { Set(Address, Port); } void cIpAddress::Set(const char *Address, int Port) { address = Address; port = Port; connection = cString::sprintf("%s:%d", *address, port); } void cIpAddress::Set(const sockaddr *SockAddr) { const sockaddr_in *Addr = (sockaddr_in *)SockAddr; Set(inet_ntoa(Addr->sin_addr), ntohs(Addr->sin_port)); } // --- cSocket --------------------------------------------------------------- #define MAXUDPBUF 1024 class cSocket { private: int port; bool tcp; int sock; cIpAddress lastIpAddress; bool IsOwnInterface(sockaddr_in *Addr); public: cSocket(int Port, bool Tcp); ~cSocket(); bool Listen(void); bool Connect(const char *Address); void Close(void); int Port(void) const { return port; } int Socket(void) const { return sock; } static bool SendDgram(const char *Dgram, int Port, const char *Address = NULL); int Accept(void); cString Discover(void); const cIpAddress *LastIpAddress(void) const { return &lastIpAddress; } }; cSocket::cSocket(int Port, bool Tcp) { port = Port; tcp = Tcp; sock = -1; } cSocket::~cSocket() { Close(); } bool cSocket::IsOwnInterface(sockaddr_in *Addr) { ifaddrs *ifaddr; if (getifaddrs(&ifaddr) >= 0) { bool Own = false; for (ifaddrs *ifa = ifaddr; ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr) { if (ifa->ifa_addr->sa_family == AF_INET) { sockaddr_in *addr = (sockaddr_in *)ifa->ifa_addr; if (addr->sin_addr.s_addr == Addr->sin_addr.s_addr) { Own = true; break; } } } } freeifaddrs(ifaddr); return Own; } else LOG_ERROR; return false; } void cSocket::Close(void) { if (sock >= 0) { close(sock); sock = -1; } } bool cSocket::Listen(void) { if (sock < 0) { // create socket: sock = tcp ? socket(PF_INET, SOCK_STREAM, IPPROTO_IP) : socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock < 0) { LOG_ERROR; return false; } // allow it to always reuse the same port: int ReUseAddr = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &ReUseAddr, sizeof(ReUseAddr)); // configure port and ip: sockaddr_in Addr; memset(&Addr, 0, sizeof(Addr)); Addr.sin_family = AF_INET; Addr.sin_port = htons(port); Addr.sin_addr.s_addr = SVDRPhosts.LocalhostOnly() ? htonl(INADDR_LOOPBACK) : htonl(INADDR_ANY); if (bind(sock, (sockaddr *)&Addr, sizeof(Addr)) < 0) { LOG_ERROR; Close(); return false; } // make it non-blocking: int Flags = fcntl(sock, F_GETFL, 0); if (Flags < 0) { LOG_ERROR; return false; } Flags |= O_NONBLOCK; if (fcntl(sock, F_SETFL, Flags) < 0) { LOG_ERROR; return false; } if (tcp) { // listen to the socket: if (listen(sock, 1) < 0) { LOG_ERROR; return false; } } isyslog("SVDRP listening on port %d/%s", port, tcp ? "tcp" : "udp"); } return true; } bool cSocket::Connect(const char *Address) { if (sock < 0 && tcp) { // create socket: sock = socket(PF_INET, SOCK_STREAM, IPPROTO_IP); if (sock < 0) { LOG_ERROR; return false; } // configure port and ip: sockaddr_in Addr; memset(&Addr, 0, sizeof(Addr)); Addr.sin_family = AF_INET; Addr.sin_port = htons(port); Addr.sin_addr.s_addr = inet_addr(Address); if (connect(sock, (sockaddr *)&Addr, sizeof(Addr)) < 0) { LOG_ERROR; Close(); return false; } // make it non-blocking: int Flags = fcntl(sock, F_GETFL, 0); if (Flags < 0) { LOG_ERROR; return false; } Flags |= O_NONBLOCK; if (fcntl(sock, F_SETFL, Flags) < 0) { LOG_ERROR; return false; } isyslog("SVDRP > %s:%d connection established", Address, port); return true; } return false; } bool cSocket::SendDgram(const char *Dgram, int Port, const char *Address) { // Create a socket: int Socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (Socket < 0) { LOG_ERROR; return false; } if (!Address) { // Enable broadcast: int One = 1; if (setsockopt(Socket, SOL_SOCKET, SO_BROADCAST, &One, sizeof(One)) < 0) { LOG_ERROR; close(Socket); return false; } } // Configure port and ip: sockaddr_in Addr; memset(&Addr, 0, sizeof(Addr)); Addr.sin_family = AF_INET; Addr.sin_addr.s_addr = Address ? inet_addr(Address) : htonl(INADDR_BROADCAST); Addr.sin_port = htons(Port); // Send datagram: dsyslog("SVDRP > %s:%d send dgram '%s'", inet_ntoa(Addr.sin_addr), Port, Dgram); int Length = strlen(Dgram); int Sent = sendto(Socket, Dgram, Length, 0, (sockaddr *)&Addr, sizeof(Addr)); if (Sent < 0) LOG_ERROR; close(Socket); return Sent == Length; } int cSocket::Accept(void) { if (sock >= 0 && tcp) { sockaddr_in Addr; uint Size = sizeof(Addr); int NewSock = accept(sock, (sockaddr *)&Addr, &Size); if (NewSock >= 0) { bool Accepted = SVDRPhosts.Acceptable(Addr.sin_addr.s_addr); if (!Accepted) { const char *s = "Access denied!\n"; if (write(NewSock, s, strlen(s)) < 0) LOG_ERROR; close(NewSock); NewSock = -1; } lastIpAddress.Set((sockaddr *)&Addr); isyslog("SVDRP < %s connection %s", lastIpAddress.Connection(), Accepted ? "accepted" : "DENIED"); } else if (FATALERRNO) LOG_ERROR; return NewSock; } return -1; } cString cSocket::Discover(void) { if (sock >= 0 && !tcp) { char buf[MAXUDPBUF]; sockaddr_in Addr; uint Size = sizeof(Addr); int NumBytes = recvfrom(sock, buf, sizeof(buf), 0, (sockaddr *)&Addr, &Size); if (NumBytes >= 0) { buf[NumBytes] = 0; if (!IsOwnInterface(&Addr)) { lastIpAddress.Set((sockaddr *)&Addr); if (!SVDRPhosts.Acceptable(Addr.sin_addr.s_addr)) { dsyslog("SVDRP < %s discovery ignored (%s)", lastIpAddress.Connection(), buf); return NULL; } if (!startswith(buf, "SVDRP:discover")) { dsyslog("SVDRP < %s discovery unrecognized (%s)", lastIpAddress.Connection(), buf); return NULL; } isyslog("SVDRP < %s discovery received (%s)", lastIpAddress.Connection(), buf); return buf; } } else if (FATALERRNO) LOG_ERROR; } return NULL; } // --- cPUTEhandler ---------------------------------------------------------- class cPUTEhandler { private: FILE *f; int status; const char *message; public: cPUTEhandler(void); ~cPUTEhandler(); bool Process(const char *s); int Status(void) { return status; } const char *Message(void) { return message; } }; cPUTEhandler::cPUTEhandler(void) { if ((f = tmpfile()) != NULL) { status = 354; message = "Enter EPG data, end with \".\" on a line by itself"; } else { LOG_ERROR; status = 554; message = "Error while opening temporary file"; } } cPUTEhandler::~cPUTEhandler() { if (f) fclose(f); } bool cPUTEhandler::Process(const char *s) { if (f) { if (strcmp(s, ".") != 0) { fputs(s, f); fputc('\n', f); return true; } else { rewind(f); if (cSchedules::Read(f)) { cSchedules::Cleanup(true); status = 250; message = "EPG data processed"; } else { status = 451; message = "Error while processing EPG data"; } fclose(f); f = NULL; } } return false; } // --- cSVDRPServer ---------------------------------------------------------- #define MAXHELPTOPIC 10 #define EITDISABLETIME 10 // seconds until EIT processing is enabled again after a CLRE command // adjust the help for CLRE accordingly if changing this! const char *HelpPages[] = { "CHAN [ + | - | | | ]\n" " Switch channel up, down or to the given channel number, name or id.\n" " Without option (or after successfully switching to the channel)\n" " it returns the current channel number and name.", "CLRE [ | | ]\n" " Clear the EPG list of the given channel number, name or id.\n" " Without option it clears the entire EPG list.\n" " After a CLRE command, no further EPG processing is done for 10\n" " seconds, so that data sent with subsequent PUTE commands doesn't\n" " interfere with data from the broadcasters.", "DELC \n" " Delete channel.", "DELR \n" " Delete the recording with the given number. Before a recording can be\n" " deleted, an LSTR command must have been executed in order to retrieve\n" " the recording numbers. The numbers don't change during subsequent DELR\n" " commands. CAUTION: THERE IS NO CONFIRMATION PROMPT WHEN DELETING A\n" " RECORDING - BE SURE YOU KNOW WHAT YOU ARE DOING!", "DELT \n" " Delete timer.", "EDIT \n" " Edit the recording with the given number. Before a recording can be\n" " edited, an LSTR command must have been executed in order to retrieve\n" " the recording numbers.", "GRAB [ [ ] ]\n" " Grab the current frame and save it to the given file. Images can\n" " be stored as JPEG or PNM, depending on the given file name extension.\n" " The quality of the grabbed image can be in the range 0..100, where 100\n" " (the default) means \"best\" (only applies to JPEG). The size parameters\n" " define the size of the resulting image (default is full screen).\n" " If the file name is just an extension (.jpg, .jpeg or .pnm) the image\n" " data will be sent to the SVDRP connection encoded in base64. The same\n" " happens if '-' (a minus sign) is given as file name, in which case the\n" " image format defaults to JPEG.", "HELP [ ]\n" " The HELP command gives help info.", "HITK [ ... ]\n" " Hit the given remote control key. Without option a list of all\n" " valid key names is given. If more than one key is given, they are\n" " entered into the remote control queue in the given sequence. There\n" " can be up to 31 keys.", "LSTC [ :groups | | | ]\n" " List channels. Without option, all channels are listed. Otherwise\n" " only the given channel is listed. If a name is given, all channels\n" " containing the given string as part of their name are listed.\n" " If ':groups' is given, all channels are listed including group\n" " separators. The channel number of a group separator is always 0.", "LSTE [ ] [ now | next | at