diff options
Diffstat (limited to 'svdrp.c')
-rw-r--r-- | svdrp.c | 620 |
1 files changed, 620 insertions, 0 deletions
diff --git a/svdrp.c b/svdrp.c new file mode 100644 index 00000000..c1347a31 --- /dev/null +++ b/svdrp.c @@ -0,0 +1,620 @@ +/* + * 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 1.1 2000/07/23 14:55:03 kls Exp $ + */ + +#define _GNU_SOURCE + +#include "svdrp.h" +#include <arpa/inet.h> +#include <ctype.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <unistd.h> +#include "config.h" +#include "interface.h" +#include "tools.h" + +// --- cSocket --------------------------------------------------------------- + +cSocket::cSocket(int Port, int Queue) +{ + port = Port; + sock = -1; +} + +cSocket::~cSocket() +{ + Close(); +} + +void cSocket::Close(void) +{ + if (sock >= 0) { + close(sock); + sock = -1; + } +} + +bool cSocket::Open(void) +{ + if (sock < 0) { + // create socket: + sock = socket(PF_INET, SOCK_STREAM, 0); + if (sock < 0) { + LOG_ERROR; + port = 0; + return false; + } + struct sockaddr_in name; + name.sin_family = AF_INET; + name.sin_port = htons(port); + name.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(sock, (struct sockaddr *)&name, sizeof(name)) < 0) { + LOG_ERROR; + Close(); + return false; + } + // make it non-blocking: + int oldflags = fcntl(sock, F_GETFL, 0); + if (oldflags < 0) { + LOG_ERROR; + return false; + } + oldflags |= O_NONBLOCK; + if (fcntl(sock, F_SETFL, oldflags) < 0) { + LOG_ERROR; + return false; + } + // listen to the socket: + if (listen(sock, queue) < 0) { + LOG_ERROR; + return false; + } + } + return true; +} + +int cSocket::Accept(void) +{ + if (Open()) { + struct sockaddr_in clientname; + uint size = sizeof(clientname); + int newsock = accept(sock, (struct sockaddr *)&clientname, &size); + if (newsock > 0) + isyslog(LOG_INFO, "connect from %s, port %hd", inet_ntoa(clientname.sin_addr), ntohs(clientname.sin_port)); + else if (errno != EINTR) + LOG_ERROR; + return newsock; + } + return -1; +} + +// --- cSVDRP ---------------------------------------------------------------- + +#define MAXCMDBUFFER 10000 +#define MAXHELPTOPIC 10 + +const char *HelpPages[] = { + "CHAN [ + | - | <number> | <name> ]\n" + " Switch channel up, down or to the given channel number or name.\n" + " Without option (or after successfully switching to the channel)\n" + " it returns the current channel number and name.", + "DELC <number>\n" + " Delete channel.", + "DELT <number>\n" + " Delete timer.", + "HELP [ <topic> ]\n" + " The HELP command gives help info.", + "LSTC [ <number> | <name> ]\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.", + "LSTT [ <number> ]\n" + " List timers. Without option, all timers are listed. Otherwise\n" + " only the given timer is listed.", + "MODC <number> <settings>\n" + " Modify a channel. Settings must be in the same format as returned\n" + " by the LSTC command.", + "MODT <number> on | off | <settings>\n" + " Modify a timer. Settings must be in the same format as returned\n" + " by the LSTT command. The special keywords 'on' and 'off' can be\n" + " used to easily activate or deactivate a timer.", + "MOVC <number> <to>\n" + " Move a channel to a new position.", + "MOVT <number> <to>\n" + " Move a timer to a new position.", + "NEWC <settings>\n" + " Create a new channel. Settings must be in the same format as returned\n" + " by the LSTC command.", + "NEWT <settings>\n" + " Create a new timer. Settings must be in the same format as returned\n" + " by the LSTT command.", + "QUIT\n" + " Exit vdr (SVDRP).\n" + " You can also hit Ctrl-D to exit.", + NULL + }; + +/* SVDRP Reply Codes: + + 214 Help message + 220 VDR service ready + 221 VDR service closing transmission channel + 250 Requested VDR action okay, completed + 451 Requested action aborted: local error in processing + 500 Syntax error, command unrecognized + 501 Syntax error in parameters or arguments + 502 Command not implemented + 504 Command parameter not implemented + 550 Requested action not taken + 554 Transaction failed + +*/ + +const char *GetHelpTopic(const char *HelpPage) +{ + static char topic[MAXHELPTOPIC]; + const char *q = HelpPage; + while (*q) { + if (isspace(*q)) { + uint n = q - HelpPage; + if (n >= sizeof(topic)) + n = sizeof(topic) - 1; + strncpy(topic, HelpPage, n); + topic[n] = 0; + return topic; + } + q++; + } + return NULL; +} + +const char *GetHelpPage(const char *Cmd) +{ + const char **p = HelpPages; + while (*p) { + const char *t = GetHelpTopic(*p); + if (strcasecmp(Cmd, t) == 0) + return *p; + p++; + } + return NULL; +} + +cSVDRP::cSVDRP(int Port) +:socket(Port) +{ + filedes = -1; + isyslog(LOG_INFO, "SVDRP listening on port %d", Port); +} + +cSVDRP::~cSVDRP() +{ + Close(); +} + +void cSVDRP::Close(void) +{ + if (filedes >= 0) { + //TODO how can we get the *full* hostname? + char buffer[MAXCMDBUFFER]; + gethostname(buffer, sizeof(buffer)); + Reply(221, "%s closing connection", buffer); + isyslog(LOG_INFO, "closing connection"); //TODO store IP#??? + close(filedes); + filedes = -1; + } +} + +bool cSVDRP::Send(const char *s, int length) +{ + if (length < 0) + length = strlen(s); + int wbytes = write(filedes, s, length); + if (wbytes == length) + return true; + if (wbytes < 0) + LOG_ERROR; + else //XXX while...??? + esyslog(LOG_ERR, "Wrote %d bytes to client while expecting %d\n", wbytes, length); + return false; +} + +void cSVDRP::Reply(int Code, const char *fmt, ...) +{ + if (filedes >= 0) { + if (Code != 0) { + va_list ap; + va_start(ap, fmt); + char *buffer; + vasprintf(&buffer, fmt, ap); + char *nl = strchr(buffer, '\n'); + if (Code > 0 && nl && *(nl + 1)) // trailing newlines don't count! + Code = -Code; + char number[16]; + sprintf(number, "%03d%c", abs(Code), Code < 0 ? '-' : ' '); + const char *s = buffer; + while (s && *s) { + const char *n = strchr(s, '\n'); + if (!(Send(number) && Send(s, n ? n - s : -1) && Send("\r\n"))) { + Close(); + break; + } + s = n ? n + 1 : NULL; + } + delete buffer; + va_end(ap); + } + else { + Reply(451, "Zero return code - looks like a programming error!"); + esyslog(LOG_ERR, "SVDRP: zero return code!"); + } + } +} + +void cSVDRP::CmdChan(const char *Option) +{ + if (*Option) { + int n = -1; + if (isnumber(Option)) { + int o = strtol(Option, NULL, 10) - 1; + if (o >= 0 && o < Channels.Count()) + n = o; + } + else if (strcmp(Option, "-") == 0) { + n = CurrentChannel; + if (CurrentChannel > 0) + n--; + } + else if (strcmp(Option, "+") == 0) { + n = CurrentChannel; + if (CurrentChannel < Channels.Count() - 1) + n++; + } + else { + int i = 0; + cChannel *channel; + while ((channel = Channels.Get(i)) != NULL) { + if (strcasecmp(channel->name, Option) == 0) { + n = i; + break; + } + i++; + } + } + if (n < 0) { + Reply(501, "Undefined channel \"%s\"", Option); + return; + } + if (Interface.Recording()) { + Reply(550, "Can't switch channel, interface is recording"); + return; + } + cChannel *channel = Channels.Get(n); + if (channel) { + if (!channel->Switch()) { + Reply(554, "Error switching to channel \"%d\"", channel->Index() + 1); + return; + } + } + else { + Reply(550, "Unable to find channel \"%s\"", Option); + return; + } + } + cChannel *channel = Channels.Get(CurrentChannel); + if (channel) + Reply(250, "%d %s", CurrentChannel + 1, channel->name); + else + Reply(550, "Unable to find channel \"%d\"", CurrentChannel); +} + +void cSVDRP::CmdDelc(const char *Option) +{ + //TODO combine this with menu action (timers must be updated) + Reply(502, "DELC not yet implemented"); +} + +void cSVDRP::CmdDelt(const char *Option) +{ + if (*Option) { + if (isnumber(Option)) { + cTimer *timer = Timers.Get(strtol(Option, NULL, 10) - 1); + if (timer) { + if (!timer->recording) { + Timers.Del(timer); + Timers.Save(); + isyslog(LOG_INFO, "timer %s deleted", Option); + Reply(250, "Timer \"%s\" deleted", Option); + } + else + Reply(550, "Timer \"%s\" is recording", Option); + } + else + Reply(501, "Timer \"%s\" not defined", Option); + } + else + Reply(501, "Error in timer number \"%s\"", Option); + } + else + Reply(501, "Missing timer number"); +} + +void cSVDRP::CmdHelp(const char *Option) +{ + if (*Option) { + const char *hp = GetHelpPage(Option); + if (hp) + Reply(214, hp); + else { + Reply(504, "HELP topic \"%s\" unknown", Option); + return; + } + } + else { + Reply(-214, "This is VDR version 0.060"); //XXX dynamically insert version number + Reply(-214, "Topics:"); + const char **hp = HelpPages; + while (*hp) { + //TODO multi-column??? + const char *topic = GetHelpTopic(*hp); + if (topic) + Reply(-214, " %s", topic); + hp++; + } + Reply(-214, "To report bugs in the implementation send email to"); + Reply(-214, " vdr-bugs@cadsoft.de"); + } + Reply(214, "End of HELP info"); +} + +void cSVDRP::CmdLstc(const char *Option) +{ + if (*Option) { + if (isnumber(Option)) { + cChannel *channel = Channels.Get(strtol(Option, NULL, 10) - 1); + if (channel) + Reply(250, "%d %s", channel->Index() + 1, channel->ToText()); + else + Reply(501, "Channel \"%s\" not defined", Option); + } + else { + int i = 0; + cChannel *next = NULL; + while (i < Channels.Count()) { + cChannel *channel = Channels.Get(i); + if (channel) { + if (strcasestr(channel->name, Option)) { + if (next) + Reply(-250, "%d %s", next->Index() + 1, next->ToText()); + next = channel; + } + } + else { + Reply(501, "Channel \"%d\" not found", i + 1); + return; + } + i++; + } + if (next) + Reply(250, "%d %s", next->Index() + 1, next->ToText()); + } + } + else { + for (int i = 0; i < Channels.Count(); i++) { + cChannel *channel = Channels.Get(i); + if (channel) + Reply(i < Channels.Count() - 1 ? -250 : 250, "%d %s", channel->Index() + 1, channel->ToText()); + else + Reply(501, "Channel \"%d\" not found", i + 1); + } + } +} + +void cSVDRP::CmdLstt(const char *Option) +{ + if (*Option) { + if (isnumber(Option)) { + cTimer *timer = Timers.Get(strtol(Option, NULL, 10) - 1); + if (timer) + Reply(250, "%d %s", timer->Index() + 1, timer->ToText()); + else + Reply(501, "Timer \"%s\" not defined", Option); + } + else + Reply(501, "Error in timer number \"%s\"", Option); + } + else { + for (int i = 0; i < Timers.Count(); i++) { + cTimer *timer = Timers.Get(i); + if (timer) + Reply(i < Timers.Count() - 1 ? -250 : 250, "%d %s", timer->Index() + 1, timer->ToText()); + else + Reply(501, "Timer \"%d\" not found", i + 1); + } + } +} + +void cSVDRP::CmdModc(const char *Option) +{ + if (*Option) { + char *tail; + int n = strtol(Option, &tail, 10); + if (tail && tail != Option) { + tail = skipspace(tail); + cChannel *channel = Channels.Get(n - 1); + if (channel) { + cChannel c = *channel; + if (!c.Parse(tail)) { + Reply(501, "Error in channel settings"); + return; + } + *channel = c; + Channels.Save(); + isyslog(LOG_INFO, "channel %d modified", channel->Index() + 1); + Reply(250, "%d %s", channel->Index() + 1, channel->ToText()); + } + else + Reply(501, "Channel \"%d\" not defined", n); + } + else + Reply(501, "Error in channel number"); + } + else + Reply(501, "Missing channel settings"); +} + +void cSVDRP::CmdModt(const char *Option) +{ + if (*Option) { + char *tail; + int n = strtol(Option, &tail, 10); + if (tail && tail != Option) { + tail = skipspace(tail); + cTimer *timer = Timers.Get(n - 1); + if (timer) { + cTimer t = *timer; + if (strcasecmp(tail, "ON") == 0) + t.active = 1; + else if (strcasecmp(tail, "OFF") == 0) + t.active = 0; + else if (!t.Parse(tail)) { + Reply(501, "Error in timer settings"); + return; + } + *timer = t; + Timers.Save(); + isyslog(LOG_INFO, "timer %d modified (%s)", timer->Index() + 1, timer->active ? "active" : "inactive"); + Reply(250, "%d %s", timer->Index() + 1, timer->ToText()); + } + else + Reply(501, "Timer \"%d\" not defined", n); + } + else + Reply(501, "Error in timer number"); + } + else + Reply(501, "Missing timer settings"); +} + +void cSVDRP::CmdMovc(const char *Option) +{ + //TODO combine this with menu action (timers must be updated) + Reply(502, "MOVC not yet implemented"); +} + +void cSVDRP::CmdMovt(const char *Option) +{ + //TODO combine this with menu action + Reply(502, "MOVT not yet implemented"); +} + +void cSVDRP::CmdNewc(const char *Option) +{ + if (*Option) { + cChannel *channel = new cChannel; + if (channel->Parse(Option)) { + Channels.Add(channel); + Channels.Save(); + isyslog(LOG_INFO, "channel %d added", channel->Index() + 1); + Reply(250, "%d %s", channel->Index() + 1, channel->ToText()); + } + else + Reply(501, "Error in channel settings"); + } + else + Reply(501, "Missing channel settings"); +} + +void cSVDRP::CmdNewt(const char *Option) +{ + if (*Option) { + cTimer *timer = new cTimer; + if (timer->Parse(Option)) { + Timers.Add(timer); + Timers.Save(); + isyslog(LOG_INFO, "timer %d added", timer->Index() + 1); + Reply(250, "%d %s", timer->Index() + 1, timer->ToText()); + } + else + Reply(501, "Error in timer settings"); + } + else + Reply(501, "Missing timer settings"); +} + +#define CMD(c) (strcasecmp(Cmd, c) == 0) + +void cSVDRP::Execute(char *Cmd) +{ + // skip leading whitespace: + Cmd = skipspace(Cmd); + // find the end of the command word: + char *s = Cmd; + while (*s && !isspace(*s)) + s++; + *s++ = 0; + if (CMD("CHAN")) CmdChan(s); + else if (CMD("DELC")) CmdDelc(s); + else if (CMD("DELT")) CmdDelt(s); + else if (CMD("HELP")) CmdHelp(s); + else if (CMD("LSTC")) CmdLstc(s); + else if (CMD("LSTT")) CmdLstt(s); + else if (CMD("MODC")) CmdModc(s); + else if (CMD("MODT")) CmdModt(s); + else if (CMD("MOVC")) CmdMovc(s); + else if (CMD("MOVT")) CmdMovt(s); + else if (CMD("NEWC")) CmdNewc(s); + else if (CMD("NEWT")) CmdNewt(s); + else if (CMD("QUIT") + || CMD("\x04")) Close(); + else Reply(500, "Command unrecognized: \"%s\"", Cmd); +} + +void cSVDRP::Process(void) +{ + bool SendGreeting = filedes < 0; + + if (filedes >= 0 || (filedes = socket.Accept()) >= 0) { + char buffer[MAXCMDBUFFER]; + if (SendGreeting) { + //TODO how can we get the *full* hostname? + gethostname(buffer, sizeof(buffer)); + time_t now = time(NULL); + Reply(220, "%s SVDRP VideoDiskRecorder 0.060; %s", buffer, ctime(&now));//XXX dynamically insert version number + } + int rbytes = readstring(filedes, buffer, sizeof(buffer) - 1); + if (rbytes > 0) { + //XXX overflow check??? + // strip trailing whitespace: + while (rbytes > 0 && strchr(" \t\r\n", buffer[rbytes - 1])) + buffer[--rbytes] = 0; + // make sure the string is terminated: + buffer[rbytes] = 0; + // showtime! + Execute(buffer); + } + else if (rbytes < 0) + Close(); + } +} + +//TODO timeout??? +//TODO more than one connection??? |