/* * 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.3 2015/09/01 10:34:34 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) const char *SVDRPHostName(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 server 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 client 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; } // --- cSVDRPClient ---------------------------------------------------------- class cSVDRPClient { private: cIpAddress ipAddress; cSocket socket; cString serverName; int timeout; cTimeMs pingTime; cFile file; int fetchFlags; void Close(void); public: cSVDRPClient(const char *Address, int Port, const char *ServerName, int Timeout); ~cSVDRPClient(); const char *ServerName(void) const { return serverName; } const char *Connection(void) const { return ipAddress.Connection(); } bool HasAddress(const char *Address, int Port) const; bool Send(const char *Command); bool Process(cStringList *Response = NULL); bool Execute(const char *Command, cStringList *Response = NULL); void SetFetchFlag(eSvdrpFetchFlags Flag); bool HasFetchFlag(eSvdrpFetchFlags Flag); }; static cPoller SVDRPClientPoller; cSVDRPClient::cSVDRPClient(const char *Address, int Port, const char *ServerName, int Timeout) :ipAddress(Address, Port) ,socket(Port, true) { serverName = ServerName; timeout = Timeout * 1000 * 9 / 10; // ping after 90% of timeout pingTime.Set(timeout); fetchFlags = sffTimers; if (socket.Connect(Address)) { if (file.Open(socket.Socket())) { SVDRPClientPoller.Add(file, false); dsyslog("SVDRP > %s client created for '%s'", ipAddress.Connection(), *serverName); SendSVDRPDiscover(Address); return; } } esyslog("SVDRP > %s ERROR: failed to create client for '%s'", ipAddress.Connection(), *serverName); } cSVDRPClient::~cSVDRPClient() { Close(); dsyslog("SVDRP > %s client destroyed for '%s'", ipAddress.Connection(), *serverName); } void cSVDRPClient::Close(void) { if (file.IsOpen()) { SVDRPClientPoller.Del(file, false); file.Close(); socket.Close(); } } bool cSVDRPClient::HasAddress(const char *Address, int Port) const { return strcmp(ipAddress.Address(), Address) == 0 && ipAddress.Port() == Port; } bool cSVDRPClient::Send(const char *Command) { pingTime.Set(timeout); dbgsvdrp("> %s: %s\n", *serverName, Command); if (safe_write(file, Command, strlen(Command) + 1) < 0) { LOG_ERROR; return false; } return true; } bool cSVDRPClient::Process(cStringList *Response) { if (file.IsOpen()) { char input[BUFSIZ]; int numChars = 0; #define SVDRPResonseTimeout 5000 // ms cTimeMs Timeout(SVDRPResonseTimeout); for (;;) { if (file.Ready(false)) { unsigned char c; int r = safe_read(file, &c, 1); if (r > 0) { if (c == '\n' || c == 0x00) { // strip trailing whitespace: while (numChars > 0 && strchr(" \t\r\n", input[numChars - 1])) input[--numChars] = 0; // make sure the string is terminated: input[numChars] = 0; dbgsvdrp("< %s: %s\n", *serverName, input); if (Response) { Response->Append(strdup(input)); if (numChars >= 4 && input[3] != '-') // no more lines will follow break; } else { switch (atoi(input)) { case 220: if (numChars > 4) { char *n = input + 4; if (char *t = strchr(n, ' ')) { *t = 0; if (strcmp(n, serverName) != 0) { serverName = n; dsyslog("SVDRP < %s remote server name is '%s'", ipAddress.Connection(), *serverName); } } } break; case 221: dsyslog("SVDRP < %s remote server closed connection to '%s'", ipAddress.Connection(), *serverName); Close(); break; } } numChars = 0; } else { if (numChars >= int(sizeof(input))) { esyslog("SVDRP < %s ERROR: out of memory", ipAddress.Connection()); Close(); break; } input[numChars++] = c; input[numChars] = 0; } Timeout.Set(SVDRPResonseTimeout); } else if (r <= 0) { isyslog("SVDRP < %s lost connection to remote server '%s'", ipAddress.Connection(), *serverName); Close(); } } else if (!Response) break; else if (Timeout.TimedOut()) { esyslog("SVDRP < %s timeout while waiting for response from '%s'", ipAddress.Connection(), *serverName); return false; } } if (pingTime.TimedOut()) Execute("PING"); } return file.IsOpen(); } bool cSVDRPClient::Execute(const char *Command, cStringList *Response) { if (Response) Response->Clear(); return Send(Command) && Process(Response); } void cSVDRPClient::SetFetchFlag(eSvdrpFetchFlags Flags) { fetchFlags |= Flags; } bool cSVDRPClient::HasFetchFlag(eSvdrpFetchFlags Flag) { bool Result = (fetchFlags & Flag); fetchFlags &= ~Flag; return Result; } // --- cSVDRPClientHandler --------------------------------------------------- class cSVDRPClientHandler : public cThread { private: cMutex mutex; cSocket udpSocket; int tcpPort; cVector clientConnections; void HandleClientConnection(void); void ProcessConnections(void); cSVDRPClient *GetClientForServer(const char *ServerName); protected: virtual void Action(void); public: cSVDRPClientHandler(int UdpPort, int TcpPort); virtual ~cSVDRPClientHandler(); void SendDiscover(const char *Address = NULL); bool Execute(const char *ServerName, const char *Command, cStringList *Response); bool GetServerNames(cStringList *ServerNames, eSvdrpFetchFlags FetchFlags = sffNone); bool TriggerFetchingTimers(const char *ServerName); }; static cSVDRPClientHandler *SVDRPClientHandler = NULL; cSVDRPClientHandler::cSVDRPClientHandler(int UdpPort, int TcpPort) :cThread("SVDRP client handler", true) ,udpSocket(UdpPort, false) { tcpPort = TcpPort; } cSVDRPClientHandler::~cSVDRPClientHandler() { Cancel(3); for (int i = 0; i < clientConnections.Size(); i++) delete clientConnections[i]; } cSVDRPClient *cSVDRPClientHandler::GetClientForServer(const char *ServerName) { for (int i = 0; i < clientConnections.Size(); i++) { if (strcmp(clientConnections[i]->ServerName(), ServerName) == 0) return clientConnections[i]; } return NULL; } void cSVDRPClientHandler::SendDiscover(const char *Address) { cMutexLock MutexLock(&mutex); cString Dgram = cString::sprintf("SVDRP:discover name:%s port:%d vdrversion:%d apiversion:%d timeout:%d", SVDRPHostName(), tcpPort, VDRVERSNUM, APIVERSNUM, Setup.SVDRPTimeout); udpSocket.SendDgram(Dgram, udpSocket.Port(), Address); } void cSVDRPClientHandler::ProcessConnections(void) { cMutexLock MutexLock(&mutex); for (int i = 0; i < clientConnections.Size(); i++) { if (!clientConnections[i]->Process()) { delete clientConnections[i]; clientConnections.Remove(i); i--; } } } void cSVDRPClientHandler::HandleClientConnection(void) { cString NewDiscover = udpSocket.Discover(); if (*NewDiscover) { cString p = strgetval(NewDiscover, "port", ':'); if (*p) { int Port = atoi(p); for (int i = 0; i < clientConnections.Size(); i++) { if (clientConnections[i]->HasAddress(udpSocket.LastIpAddress()->Address(), Port)) { dsyslog("SVDRP < %s connection to '%s' confirmed", clientConnections[i]->Connection(), clientConnections[i]->ServerName()); return; } } cString ServerName = strgetval(NewDiscover, "name", ':'); if (*ServerName) { cString t = strgetval(NewDiscover, "timeout", ':'); if (*t) { int Timeout = atoi(t); if (Timeout > 10) // don't let it get too small clientConnections.Append(new cSVDRPClient(udpSocket.LastIpAddress()->Address(), Port, ServerName, Timeout)); else esyslog("SVDRP < %s ERROR: invalid timeout (%d)", udpSocket.LastIpAddress()->Connection(), Timeout); } else esyslog("SVDRP < %s ERROR: missing timeout", udpSocket.LastIpAddress()->Connection()); } else esyslog("SVDRP < %s ERROR: missing server name", udpSocket.LastIpAddress()->Connection()); } else esyslog("SVDRP < %s ERROR: missing port number", udpSocket.LastIpAddress()->Connection()); } } void cSVDRPClientHandler::Action(void) { if (udpSocket.Listen()) { SVDRPClientPoller.Add(udpSocket.Socket(), false); SendDiscover(); while (Running()) { SVDRPClientPoller.Poll(1000); cMutexLock MutexLock(&mutex); HandleClientConnection(); ProcessConnections(); } SVDRPClientPoller.Del(udpSocket.Socket(), false); udpSocket.Close(); } } bool cSVDRPClientHandler::Execute(const char *ServerName, const char *Command, cStringList *Response) { cMutexLock MutexLock(&mutex); if (cSVDRPClient *Client = GetClientForServer(ServerName)) return Client->Execute(Command, Response); return false; } bool cSVDRPClientHandler::GetServerNames(cStringList *ServerNames, eSvdrpFetchFlags FetchFlag) { cMutexLock MutexLock(&mutex); ServerNames->Clear(); for (int i = 0; i < clientConnections.Size(); i++) { cSVDRPClient *Client = clientConnections[i]; if (FetchFlag == sffNone || Client->HasFetchFlag(FetchFlag)) ServerNames->Append(strdup(Client->ServerName())); } return ServerNames->Size() > 0; } bool cSVDRPClientHandler::TriggerFetchingTimers(const char *ServerName) { cMutexLock MutexLock(&mutex); if (cSVDRPClient *Client = GetClientForServer(ServerName)) { Client->SetFetchFlag(sffTimers); return true; } return false; } // --- 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