summaryrefslogtreecommitdiff
path: root/svdrp.c
diff options
context:
space:
mode:
Diffstat (limited to 'svdrp.c')
-rw-r--r--svdrp.c620
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???