[MPG123]: DCCP support for mpg123

This adds DCCP support to mpg123, which is enabled via pseudo-URIs of the type
	dccp://10.10.1.2:8000		# host:port
	dccp://10.10.1.2		# host with default port 8000
	dccp://localhost		# hostname (for IPv4 or IPv6 host)
	dccp://localhost:8001		# hostname with port
	dccp://[::1]:8000 		# same, but different syntax
	dccp://[badc0de::1]		# IPv6 host with default port 8000

This version patches against mpg123-1.3.1

To transform the datagram-stream of DCCP into the byte-stream expected by mpg123,
a pipe(2) is used, which is asynchronously re-filled by a separate producer 
process. The producer runs as child process and closes the pipe when the DCCP
connection signals End-of-File.

Since this is Linux-specific code (no other OS currently supports DCCP), dealing
with child processes has been simplified by setting the handler for SIGCHLD to
SIG_IGN (Linux and Posix take this as a condition that terminating children do
not become zombie processes).

As in other uses of streaming, it is good to use the buffer option of mpg123
(`-b' argument which takes a size in kilobytes): in experimental test runs it
was found that values greater than 40 .. 50 are usually sufficient to allow
smooth streaming. E.g.

		mpg123  -b50  dccp://other_host:9011

Gerrit Renker
---
 src/Makefile.am |    1
 src/Makefile.am |    1 
 src/Makefile.in |    3 
 src/common.c    |    3 
 src/dccp.c      |  183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/mpg123.c    |    4 +
 src/mpg123app.h |    4 +
 src/playlist.c  |   19 +++++
 7 files changed, 214 insertions(+), 3 deletions(-)

--- a/src/mpg123app.h
+++ b/src/mpg123app.h
@@ -186,4 +186,8 @@ void prev_track(void);
 int  open_track(char *fname);
 void close_track(void);
 void set_intflag(void);
+
+/* ------ Declarations from "dccp.c" ------ */
+/* takes url (IPv4/6 address / hostname + optional port) */
+extern int  dccp_open(const char* url);
 #endif 
--- /dev/null
+++ b/src/dccp.c
@@ -0,0 +1,183 @@
+/*
+ *	DCCP Plug-In for mpg123
+ *	Works in the same way as the HTTP plugin; it connects to a server serving
+ *	at a specified port, and expects a stream of MP3 bytes coming from that server.
+ *	It turns the stream of datagrams into a bytestream, as expected by the decoder,
+ *	using a separate producer process shifting the output from the socket into a pipe.
+ */
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <stdint.h>
+#include <netdb.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <err.h>
+#include <ctype.h>
+
+#include "mpg123app.h"
+
+/* DCCP-specific defines which may not appear in all header files */
+#ifndef IPPROTO_DCCP
+#define IPPROTO_DCCP		33	/* IANA assigned value */
+#define SOCK_DCCP		6	/* Linux socket type */
+#define SOL_DCCP		269	/* Linux socket level */
+#endif
+
+/* The fallback port if none is given */
+#define DCCP_DEFAULT_PORT	"8000"
+
+/*
+ * We use a single pipe to emulate a bytestream with a datagram-based protocol.
+ * This is ok since this program can only read one source at a time.
+ */
+static	int pipeline[2];
+
+/*
+ * For a reader process which continually receives from the socket, while
+ * the parent process reads from the output end of the pipe.
+ */
+static int dccp_pipeline(int sockfd)
+{
+	char	buf[PIPE_BUF];
+	ssize_t len = read(sockfd, buf, sizeof(buf));
+
+	if (len <= 0)		/* Connection has failed or been reset */
+		return -1;
+
+	if (pipe(pipeline) < 0)
+		err(1, "DCCP pipeline");
+	/*
+	 * The producer runs in the child process. Setting the handler for SIGCHLD
+	 * to SIG_IGN avoids that terminating children become zombies.
+	 */
+	if (signal(SIGCHLD, SIG_IGN) < 0)
+		err(1, "Signal handler");
+
+	switch(fork()) {
+	case -1:
+		err(1, "Can not fork DCCP producer");
+	case 0:
+		/*
+		 * Child process: closes the read end and
+		 * moves data from sockfd into the write end
+		 */
+		close(pipeline[0]);
+		do  {
+			write(pipeline[1], buf, len);
+			len = read(sockfd, buf, sizeof(buf));
+		} while (len > 0);
+		/* Done reading: signal EOF to parent process by closing the pipe */
+		close(sockfd);
+		close(pipeline[1]);
+		return 0;
+	}
+	/* The parent process only retains the read end as open descriptor */
+	close(sockfd);
+	close(pipeline[1]);
+	return pipeline[0];
+}
+
+static int dccp_connect(const char *host, const char *port)
+{
+	struct addrinfo *remote = NULL, *dst, hints;
+	int rc, sockfd = -1;
+
+	/* Set up address hint structure */
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	/* getaddrinfo does not really work well with SOCK_DCCP - ignore ai_socktype */
+
+	/* only use addresses available on the host */
+	/* use v4-mapped-v6 if no v6 addresses found */
+	hints.ai_flags = AI_ADDRCONFIG | AI_V4MAPPED | AI_ALL;
+
+	/* Obtain local/remote address information */
+	if ((rc = getaddrinfo(host, port, &hints, &remote)))
+		errx(1, "Can not resolve DCCP address %s#%s: %s",
+			host, port, gai_strerror(rc));
+
+	for (dst = remote; dst != NULL; dst = dst->ai_next) {
+		sockfd = socket(dst->ai_family, SOCK_DCCP, IPPROTO_DCCP);
+		if (sockfd < 0)
+			continue;
+
+		if (connect(sockfd, dst->ai_addr, dst->ai_addrlen) == 0)
+			break;
+		close(sockfd);
+	}
+	freeaddrinfo(remote);
+
+	if (dst == NULL)
+		err(1, "Can not create DCCP socket %s#%s", host, port);
+	/*
+	 * Optimisation: we know we won't be writing any data to the server. We thus
+	 * shut down the write end, which causes the kernel module for the client-server
+	 * half-connection to be shut down. This reduces internal processing costs.
+	 */
+	if (shutdown(sockfd, SHUT_WR) < 0)
+		err(1, "Can not close write end.");
+
+	return sockfd;
+}
+
+static int dccp_setup_reader(const char *host, const char *port)
+{
+	int fd = dccp_connect(host, port);
+
+	if (fd < 0)		/* connection setup failed */
+		exit(1);
+
+	fd = dccp_pipeline(fd);
+	if (fd < 0)
+		errx(1, "Can not read DCCP data from %s#%s", host, port);
+	if (fd == 0)		/* reader process has finished */
+		_exit(0);
+	return fd;
+}
+
+/*
+ * Handler for URLs of the type
+ *	dccp://x.x.x.x[:port]		// IPv4 address with optional port
+ *	dccp://hostname[:port]		// hostname plus optional port
+ *	dccp://[x:x:x:...:x][:port]	// IPv6 address with optional port
+ */
+int dccp_open(const char* url)
+{
+	char *host, *port, *copy_url, *ptr;
+	int  fd;
+
+	/* string needs to start with dccp://host-part and host-part must not be empty */
+	if (strncmp(url, "dccp://", 7) || strlen(url) <= 7)
+		goto no_match;
+	host = ptr = copy_url = strdup(url + 7);
+
+	if (*host == '[') {			/* dccp://[IPv6-address] (RFC 3986) */
+		ptr = strrchr(host++, ']');
+		if (ptr == NULL)		/* missing right bracket */
+			goto no_match;
+		*ptr++ = '\0';
+	}
+
+	/* the last ':' in the string separates host:port */
+	port = strrchr(ptr, ':');
+	if (port == NULL)
+		port = DCCP_DEFAULT_PORT;
+	else if (port == host || strlen(port) < 2 || strchr(ptr, ':') != port)
+		/* empty hostname/port or IPv6 ambiguity (e.g. dccp://abcd::1) */
+		goto no_match;
+	else	/* non-empty port specified */
+		*port++ = '\0';
+
+	fd = dccp_setup_reader(host, port);
+	free(copy_url);
+	return fd;
+
+no_match:
+	errx(1, "The URI \"%s\" does not match dccp://host[:port] pattern (RFC 3986).", url);
+}
--- a/src/mpg123.c
+++ b/src/mpg123.c
@@ -500,6 +500,10 @@ int open_track(char *fname)
 		error1("Cannot set ICY interval: %s", mpg123_strerror(mh));
 		if(param.verbose > 1) fprintf(stderr, "Info: ICY interval %li\n", (long)htd.icy_interval);
 	}
+	else if (!strncmp(fname, "dccp://", 7)) /* dccp stream of datagrams (not a bytestream)  */
+	{
+		filept = dccp_open(fname);
+	}
 	debug("OK... going to finally open.");
 	/* Now hook up the decoder on the opened stream or the file. */
 	if(filept > -1)
--- a/src/playlist.c
+++ b/src/playlist.c
@@ -184,6 +184,8 @@ int add_next_file (int argc, char *argv[
 	if (param.listname || pl.file)
 	{
 		size_t line_offset = 0;
+		int fd;
+
 		if (!pl.file)
 		{
 			/* empty or "-" */
@@ -195,7 +197,6 @@ int add_next_file (int argc, char *argv[
 			}
 			else if (!strncmp(param.listname, "http://", 7))
 			{
-				int fd;
 				struct httpdata htd;
 				httpdata_init(&htd);
 				fd = http_open(param.listname, &htd);
@@ -243,6 +244,22 @@ int add_next_file (int argc, char *argv[
 					pl.file = fdopen(fd,"r");
 				}
 			}
+			else if (!strncmp(param.listname, "dccp://", 7))
+			{
+				fd = dccp_open(param.listname);
+
+				if(fd < 0)
+				{
+					param.listname = NULL;
+					pl.file = NULL;
+					fprintf(stderr, "Error: invalid playlist from dccp_open()!\n");
+				}
+				else
+				{
+					pl.entry = 0;
+					pl.file = fdopen(fd,"r");
+				}
+			}
 			else if (!(pl.file = fopen(param.listname, "rb")))
 			{
 				perror (param.listname);
--- a/src/common.c
+++ b/src/common.c
@@ -166,7 +166,8 @@ int split_dir_file (const char *path, ch
 			if (lastdir)
 				free (lastdir);
 			lastdir = *dname;
-			return 1;
+			/* http:// or dccp:// contain '/' */
+			return strrchr(path, ':') == NULL;
 		}
 	}
 	else {
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -33,6 +33,7 @@ mpg123_SOURCES = \
 	getlopt.h \
 	httpget.c \
 	httpget.h \
+	dccp.c \
 	genre.h \
 	genre.c \
 	module.h \
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -53,7 +53,7 @@ binPROGRAMS_INSTALL = $(INSTALL_PROGRAM)
 PROGRAMS = $(bin_PROGRAMS)
 am_mpg123_OBJECTS = audio.$(OBJEXT) buffer.$(OBJEXT) common.$(OBJEXT) \
 	compat.$(OBJEXT) control_generic.$(OBJEXT) getlopt.$(OBJEXT) \
-	httpget.$(OBJEXT) genre.$(OBJEXT) mpg123.$(OBJEXT) \
+	httpget.$(OBJEXT) dccp.$(OBJEXT) genre.$(OBJEXT) mpg123.$(OBJEXT) \
 	id3print.$(OBJEXT) playlist.$(OBJEXT) sfifo.$(OBJEXT) \
 	term.$(OBJEXT) wav.$(OBJEXT) xfermem.$(OBJEXT)
 mpg123_OBJECTS = $(am_mpg123_OBJECTS)
@@ -320,6 +320,7 @@ mpg123_SOURCES = \
 	getlopt.h \
 	httpget.c \
 	httpget.h \
+	dccp.c \
 	genre.h \
 	genre.c \
 	module.h \
