From a2fb5d219d5ceccfeb6c93d2ab83724ac57cc35e Mon Sep 17 00:00:00 2001 From: James Laird Date: Thu, 28 Mar 2013 00:15:22 +1100 Subject: [PATCH] Refactor and rewrite. Perl script is removed in favour of C. Stale documentation and initfiles are removed for now also. C interface works, but still needs work (for example, mDNS is missing) - see TODO. --- FreeBSD.notes | 4 - INSTALL.md | 106 --- Makefile | 42 +- TODO | 13 + alac.c | 12 +- alac.h | 12 +- audio.h | 22 + audio_ao.c | 53 ++ audio_dummy.c | 52 ++ common.c | 143 ++++ common.h | 29 + hairtunes.c | 964 ------------------------ hairtunes.h | 11 - org.mafipulation.shairport.plist | 16 - player.c | 478 ++++++++++++ player.h | 22 + rtp.c | 157 ++++ rtp.h | 10 + rtsp.c | 723 ++++++++++++++++++ rtsp.h | 7 + shairport.c | 1212 +----------------------------- shairport.h | 57 -- shairport.init.sample | 67 -- shairport.pl | 763 ------------------- socketlib.c | 277 ------- socketlib.h | 39 - 26 files changed, 1767 insertions(+), 3524 deletions(-) delete mode 100644 FreeBSD.notes delete mode 100644 INSTALL.md create mode 100644 TODO create mode 100644 audio.h create mode 100644 audio_ao.c create mode 100644 audio_dummy.c create mode 100644 common.c create mode 100644 common.h delete mode 100644 hairtunes.c delete mode 100644 hairtunes.h delete mode 100644 org.mafipulation.shairport.plist create mode 100644 player.c create mode 100644 player.h create mode 100644 rtp.c create mode 100644 rtp.h create mode 100644 rtsp.c create mode 100644 rtsp.h delete mode 100644 shairport.h delete mode 100755 shairport.init.sample delete mode 100755 shairport.pl delete mode 100644 socketlib.c delete mode 100644 socketlib.h diff --git a/FreeBSD.notes b/FreeBSD.notes deleted file mode 100644 index 34cffabd..00000000 --- a/FreeBSD.notes +++ /dev/null @@ -1,4 +0,0 @@ -/usr/ports/security/p5-Crypt-RSA -/usr/ports/security/p5-Crypt-OpenSSL-RSA -net/p5-Net-SDP -/usr/ports/net/p5-IO-Socket-INET6 diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index 1a8fbee9..00000000 --- a/INSTALL.md +++ /dev/null @@ -1,106 +0,0 @@ -Type `make` to build the packet decoder, `hairtunes`. - -You need the following installed: - - * openssl - * libao - * Perl 5.10 or higher - * Linux: avahi (or, for embedded systems, howl) - * Windows/Mac OS X: Bonjour - -Perl modules (install from CPAN if needed e.g. `perl -MCPAN -e 'install X'`): - - * HTTP::Request - * HTTP::Message - * Crypt::OpenSSL::RSA - * IO::Socket::INET6 - * Net::SDP - -## Debian/Ubuntu: - - apt-get install build-essential libssl-dev libcrypt-openssl-rsa-perl libao-dev libio-socket-inet6-perl libwww-perl avahi-utils pkg-config - make - perl shairport.pl - -## Redhat/Fedora: - - yum install openssl-devel libao libao-devel perl-Crypt-OpenSSL-RSA perl-IO-Socket-INET6 perl-libwww-perl avahi-tools - make - perl shairport.pl - -## Gentoo/Funtoo - - echo "media-libs/libao alsa" >> /etc/portage/package.use - echo "net-dns/avahi mdnsresponder-compat" >> /etc/portage/package.use - echo "dev-perl/HTTP-Message ~x86" >> /etc/portage/package.keywords - echo "dev-perl/HTTP-Date ~x86" >> /etc/portage/package.keywords - echo "dev-perl/Encode-Locale ~x86" >> /etc/portage/package.keywords - echo "dev-perl/LWP-MediaTypes ~x86" >> /etc/portage/package.keywords - emerge libao avahi dev-perl/HTTP-Message dev-perl/Crypt-OpenSSL-RSA dev-perl/IO-Socket-INET6 - rc-update add dbus default - rc-update add avahi-daemon default - /etc/init.d/dbus start - /etc/init.d/avahi-daemon start - make - perl shairport.pl - -## OpenBSD - - pkg_add -r gmake libao avahi p5-libwww p5-Crypt-OpenSSL-RSA p5-IO-Socket-INET6 - gmake - perl shairport.pl - -## FreeBSD - - pkg_add -r gmake libao avahi p5-libwww p5-Crypt-OpenSSL-RSA p5-Net-SDP p5-IO-Socket-INET6 - gmake - # ipv6 does not work for me, but maybe works for you? might be due to not - # having local ipv6 working dns names? - # force ipv4 on FreeBSD with '-4' on the command line. - perl shairport.pl -4 - -## Mac OS X: - - * install XCode - * install [Homebrew](https://github.com/mxcl/homebrew) or [MacPorts](http://www.macports.org/) - * type: - - $ export ARCHFLAGS="-arch x86_64" # (replace x86_64 by your arch) - $ brew install pkg-config libao # for [Homebrew](https://github.com/mxcl/homebrew) - $ port install pkgconfig libao # for [MacPorts](http://www.macports.org/) - $ make - $ perl -MCPAN -e 'install Crypt::OpenSSL::RSA' # (may require sudo) - $ perl -MCPAN -e 'install IO::Socket::INET6' # (may require sudo) - $ perl shairport.pl - - Users of OS X 10.5 and below will need to install a newer Perl (via `port`/`brew`). - -### How to run as a daemon on Mac 10.6 - - $ cp hairtunes shairport.pl /usr/local/bin - $ vi /usr/local/bin/shairport.pl # change the path of hairtunes from ./hairtunes to /usr/local/bin/hairtunes - $ mkdir -p ~/Library/LaunchAgents - $ cp org.mafipulation.shairport.plist ~/Library/LaunchAgents/ - $ launchctl load org.mafipulation.shairport.plist - $ launchctl unload org.mafipulation.shairport.plist # (to remove) - -## Windows - - * Download and install Cygwin. - * During setup, select - * openssl-devel - * openssl - * libao - * libao-devel - * gcc4 - * make - * pkg-config - * perl - * Install [Bonjour for Windows](http://support.apple.com/kb/DL999) - * Launch the Cygwin Bash shell - * type: - - $ make - $ perl -MCPAN -e 'install Crypt::OpenSSL::RSA' - $ perl -MCPAN -e 'install IO::Socket::INET6' - $ perl shairport.pl diff --git a/Makefile b/Makefile index f3888d14..58996c3c 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,6 @@ +SRCS := shairport.c rtsp.c common.c rtp.c player.c alac.c $(wildcard audio_*.c) -MY_CFLAGS= $(shell pkg-config --cflags ao) -MY_LDFLAGS= $(shell pkg-config --libs ao) -ifeq ($(shell uname),FreeBSD) -MY_LDFLAGS+= -lssl -else -MY_CFLAGS+= $(shell pkg-config --cflags openssl) -MY_LDFLAGS+= $(shell pkg-config --libs openssl) -endif - -CFLAGS:=-O2 -Wall $(MY_CFLAGS) -LDFLAGS:=-lm -lpthread $(MY_LDFLAGS) - -OBJS=socketlib.o shairport.o alac.o hairtunes.o -all: hairtunes shairport - -hairtunes: hairtunes.c alac.o - $(CC) $(CFLAGS) -DHAIRTUNES_STANDALONE hairtunes.c alac.o -o $@ $(LDFLAGS) - -shairport: $(OBJS) - $(CC) $(CFLAGS) $(OBJS) -o $@ $(LDFLAGS) - -clean: - -@rm -rf hairtunes shairport $(OBJS) - - -%.o: %.c - $(CC) $(CFLAGS) -c $< -o $@ - - -prefix=/usr/local -install: hairtunes shairport - install -D -m 0755 hairtunes $(DESTDIR)$(prefix)/bin/hairtunes - install -D -m 0755 shairport.pl $(DESTDIR)$(prefix)/bin/shairport.pl - install -D -m 0755 shairport $(DESTDIR)$(prefix)/bin/shairport - -.PHONY: all clean install - -.SILENT: clean +LIBS := -lcrypto -lm -lao -lpthread +shairport: $(SRCS) + gcc -ggdb -Wall $(SRCS) $(LIBS) -o shairport diff --git a/TODO b/TODO new file mode 100644 index 00000000..1ad7161e --- /dev/null +++ b/TODO @@ -0,0 +1,13 @@ +- make BUFFER_FRAMES adjustable +- use getopt +- copyright updates +- signal handlers +- pluggable audio drivers +- audio usage +- finish ao +- makefile +- documentation +- PKGBUILD, initscript, systemd, launchd plist +- mDNS +- volume control +- tidy debug printing diff --git a/alac.c b/alac.c index 466b560d..8ca7a1cc 100644 --- a/alac.c +++ b/alac.c @@ -55,7 +55,7 @@ static const int host_bigendian = 0; struct {signed int x:24;} se_struct_24; #define SignExtend24(val) (se_struct_24.x = val) -void allocate_buffers(alac_file *alac) +void alac_allocate_buffers(alac_file *alac) { alac->predicterror_buffer_a = malloc(alac->setinfo_max_samples_per_frame * 4); alac->predicterror_buffer_b = malloc(alac->setinfo_max_samples_per_frame * 4); @@ -110,7 +110,7 @@ void alac_set_info(alac_file *alac, char *inputbuffer) if (!host_bigendian) _Swap32(alac->setinfo_8a_rate); - allocate_buffers(alac); + alac_allocate_buffers(alac); } @@ -687,9 +687,9 @@ static void deinterlace_24(int32_t *buffer_a, int32_t *buffer_b, } -void decode_frame(alac_file *alac, - unsigned char *inbuffer, - void *outbuffer, int *outputsize) +void alac_decode_frame(alac_file *alac, + unsigned char *inbuffer, + void *outbuffer, int *outputsize) { int channels; int32_t outputsamples = alac->setinfo_max_samples_per_frame; @@ -1104,7 +1104,7 @@ void decode_frame(alac_file *alac, } } -alac_file *create_alac(int samplesize, int numchannels) +alac_file *alac_create(int samplesize, int numchannels) { alac_file *newfile = malloc(sizeof(alac_file)); diff --git a/alac.h b/alac.h index dd130681..ce9bf8dc 100644 --- a/alac.h +++ b/alac.h @@ -1,14 +1,16 @@ #ifndef __ALAC__DECOMP_H #define __ALAC__DECOMP_H +#include + typedef struct alac_file alac_file; -alac_file *create_alac(int samplesize, int numchannels); -void decode_frame(alac_file *alac, - unsigned char *inbuffer, - void *outbuffer, int *outputsize); +alac_file *alac_create(int samplesize, int numchannels); +void alac_decode_frame(alac_file *alac, + unsigned char *inbuffer, + void *outbuffer, int *outputsize); void alac_set_info(alac_file *alac, char *inputbuffer); -void allocate_buffers(alac_file *alac); +void alac_allocate_buffers(alac_file *alac); struct alac_file { diff --git a/audio.h b/audio.h new file mode 100644 index 00000000..a1d65c8f --- /dev/null +++ b/audio.h @@ -0,0 +1,22 @@ +#ifndef _AUDIO_H +#define _AUDIO_H + +typedef struct { + // start of program + int (*init)(int argc, char **argv); + // at end of program + void (*deinit)(void); + + void (*start)(int sample_rate); + // block of samples + void (*play)(short buf[], int samples); + void (*stop)(void); + + // may be NULL, in which case soft volume is applied + void (*volume)(double vol); +} audio_ops; + +extern audio_ops audio_dummy; +extern audio_ops audio_ao; + +#endif //_AUDIO_H diff --git a/audio_ao.c b/audio_ao.c new file mode 100644 index 00000000..704b63b6 --- /dev/null +++ b/audio_ao.c @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include "common.h" +#include "audio.h" + +ao_device *dev = NULL; + +static int init(int argc, char **argv) { + printf("ao: init\n"); + ao_initialize(); + ao_sample_format fmt; + memset(&fmt, 0, sizeof(fmt)); + + fmt.bits = 16; + fmt.rate = 44100; + fmt.channels = 2; + fmt.byte_format = AO_FMT_NATIVE; + + int driver = ao_default_driver_id(); + + dev = ao_open_live(driver, &fmt, NULL); + + return dev ? 0 : 1; +} + +static void deinit(void) { + if (dev) + ao_close(dev); +} + +static void start(int sample_rate) { + printf("ao: start\n"); + if (sample_rate != 44100) + die("unexpceted sample rate!\n"); +} + +static void play(short buf[], int samples) { + ao_play(dev, (char*)buf, samples*4); +} + +static void stop(void) { +} + +audio_ops audio_ao = { + .init = &init, + .deinit = &deinit, + .start = &start, + .stop = &stop, + .play = &play, + .volume = NULL +}; diff --git a/audio_dummy.c b/audio_dummy.c new file mode 100644 index 00000000..7eab0544 --- /dev/null +++ b/audio_dummy.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include "audio.h" + +int Fs; +long long starttime, samples_played; + +static int dummy_init(int argc, char **argv) { + return 0; +} + +static void dummy_deinit(void) { +} + +static void dummy_start(int sample_rate) { + Fs = sample_rate; + starttime = 0; + samples_played = 0; + printf("dummy audio output started at Fs=%d Hz\n", sample_rate); +} + +static void dummy_play(short buf[], int samples) { + struct timeval tv; + + // this is all a bit expensive but it's long-term stable. + gettimeofday(&tv, NULL); + + long long nowtime = tv.tv_usec + 1e6*tv.tv_sec; + + if (!starttime) + starttime = nowtime; + + samples_played += samples; + + long long finishtime = starttime + samples_played * 1e6 / Fs; + + usleep(finishtime - nowtime); +} + +static void dummy_stop(void) { + printf("dummy audio stopped\n"); +} + +audio_ops audio_dummy = { + .init = &dummy_init, + .deinit = &dummy_deinit, + .start = &dummy_start, + .stop = &dummy_stop, + .play = &dummy_play, + .volume = NULL +}; diff --git a/common.c b/common.c new file mode 100644 index 00000000..52ea12d2 --- /dev/null +++ b/common.c @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common.h" + +shairport_cfg config; + +int debuglev = 1; + +void die(char *format, ...) { + fprintf(stderr, "FATAL: "); + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); + exit(1); +} + +void warn(char *format, ...) { + fprintf(stderr, "WARNING: "); + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + +void debug(char *format, ...) { + if (!debuglev) + return; + fprintf(stderr, "DEBUG: "); + va_list args; + va_start(args, format); + vfprintf(stderr, format, args); + va_end(args); +} + + +char *base64_enc(uint8_t *input, int length) { + int ret; + BIO *bmem, *b64; + BUF_MEM *bptr; + b64 = BIO_new(BIO_f_base64()); + bmem = BIO_new(BIO_s_mem()); + b64 = BIO_push(b64, bmem); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO_write(b64, input, length); + ret = BIO_flush(b64); + BIO_get_mem_ptr(b64, &bptr); + + char *buf = (char *)malloc(bptr->length); + if (bptr->length) { + memcpy(buf, bptr->data, bptr->length-1); + buf[bptr->length-1] = 0; + } + + BIO_free_all(bmem); + + return buf; +} + +uint8_t *base64_dec(char *input, int *outlen) { + int ret; + BIO *bmem, *b64; + int inlen = strlen(input); + + b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + bmem = BIO_new(BIO_s_mem()); + b64 = BIO_push(b64, bmem); + + // Apple cut the padding off their challenges; restore it + BIO_write(bmem, input, inlen); + while (inlen++ & 3) + BIO_write(bmem, "=", 1); + ret = BIO_flush(bmem); + + int bufsize = strlen(input)*3/4 + 1; + uint8_t *buf = malloc(bufsize); + int nread; + + nread = BIO_read(b64, buf, bufsize); + + BIO_free_all(bmem); + + *outlen = nread; + return buf; +} + +static char super_secret_key[] = +"-----BEGIN RSA PRIVATE KEY-----\n" +"MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n" +"wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n" +"wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n" +"/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n" +"UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n" +"BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n" +"LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n" +"NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n" +"lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n" +"aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n" +"a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n" +"oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n" +"oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n" +"k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n" +"AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n" +"cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n" +"54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n" +"17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n" +"1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n" +"LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n" +"2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n" +"-----END RSA PRIVATE KEY-----"; + +uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode) { + static RSA *rsa = NULL; + + if (!rsa) { + BIO *bmem = BIO_new_mem_buf(super_secret_key, -1); + rsa = PEM_read_bio_RSAPrivateKey(bmem, NULL, NULL, NULL); + BIO_free(bmem); + } + + uint8_t *out = malloc(RSA_size(rsa)); + switch (mode) { + case RSA_MODE_AUTH: + *outlen = RSA_private_encrypt(inlen, input, out, rsa, + RSA_PKCS1_PADDING); + break; + case RSA_MODE_KEY: + *outlen = RSA_private_decrypt(inlen, input, out, rsa, + RSA_PKCS1_OAEP_PADDING); + break; + default: + die("bad rsa mode"); + } + return out; +} diff --git a/common.h b/common.h new file mode 100644 index 00000000..94780e5a --- /dev/null +++ b/common.h @@ -0,0 +1,29 @@ +#ifndef _COMMON_H +#define _COMMON_H + +#include +#include +#include "audio.h" + +typedef struct { + char *password; + char hw_addr[6]; + int port; + audio_ops *output; + int buffer_start_fill; +} shairport_cfg; + +void die(char *format, ...); +void warn(char *format, ...); +void debug(char *format, ...); + +uint8_t *base64_dec(char *input, int *outlen); +char *base64_enc(uint8_t *input, int length); + +#define RSA_MODE_AUTH (0) +#define RSA_MODE_KEY (1) +uint8_t *rsa_apply(uint8_t *input, int inlen, int *outlen, int mode); + +extern shairport_cfg config; + +#endif // _COMMON_H diff --git a/hairtunes.c b/hairtunes.c deleted file mode 100644 index 80265c05..00000000 --- a/hairtunes.c +++ /dev/null @@ -1,964 +0,0 @@ -/* - * HairTunes - RAOP packet handler and slave-clocked replay engine - * Copyright (c) James Laird 2011 - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "hairtunes.h" -#include -#include -#include - -#ifdef FANCY_RESAMPLING -#include -#endif - -#include -static int debug = 0; - -#include "alac.h" - -// and how full it needs to be to begin (must be decoder -static double volume = 1.0; -static int fix_volume = 0x10000; -static pthread_mutex_t vol_mutex = PTHREAD_MUTEX_INITIALIZER; - - - -typedef struct audio_buffer_entry { // decoded audio packets - int ready; - signed short *data; -} abuf_t; -static abuf_t audio_buffer[BUFFER_FRAMES]; -#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) - -// mutex-protected variables -static seq_t ab_read, ab_write; -static int ab_buffering = 1, ab_synced = 0; -static pthread_mutex_t ab_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t ab_buffer_ready = PTHREAD_COND_INITIALIZER; - -static void die(char *why) { - fprintf(stderr, "FATAL: %s\n", why); - exit(1); -} - -#ifdef HAIRTUNES_STANDALONE -static int hex2bin(unsigned char *buf, char *hex) { - int i, j; - if (strlen(hex) != 0x20) - return 1; - for (i=0; i<0x10; i++) { - if (!sscanf(hex, "%2X", &j)) - return 1; - hex += 2; - *buf++ = j; - } - return 0; -} -#endif - -static int init_decoder(void) { - alac_file *alac; - - frame_size = fmtp[1]; // stereo samples - sampling_rate = fmtp[11]; - - int sample_size = fmtp[3]; - if (sample_size != 16) - die("only 16-bit samples supported!"); - - alac = create_alac(sample_size, 2); - if (!alac) - return 1; - decoder_info = alac; - - alac->setinfo_max_samples_per_frame = frame_size; - alac->setinfo_7a = fmtp[2]; - alac->setinfo_sample_size = sample_size; - alac->setinfo_rice_historymult = fmtp[4]; - alac->setinfo_rice_initialhistory = fmtp[5]; - alac->setinfo_rice_kmodifier = fmtp[6]; - alac->setinfo_7f = fmtp[7]; - alac->setinfo_80 = fmtp[8]; - alac->setinfo_82 = fmtp[9]; - alac->setinfo_86 = fmtp[10]; - alac->setinfo_8a_rate = fmtp[11]; - allocate_buffers(alac); - return 0; -} - -int hairtunes_init(char *pAeskey, char *pAesiv, char *fmtpstr, int pCtrlPort, int pTimingPort, - int pDataPort, char *pRtpHost, char*pPipeName, char *pLibaoDriver, char *pLibaoDeviceName, char *pLibaoDeviceId, - int bufStartFill) -{ - if(pAeskey != NULL) - memcpy(aeskey, pAeskey, sizeof(aeskey)); - if(pAesiv != NULL) - memcpy(aesiv, pAesiv, sizeof(aesiv)); - if(pRtpHost != NULL) - rtphost = pRtpHost; - if(pPipeName != NULL) - pipename = pPipeName; - if(pLibaoDriver != NULL) - libao_driver = pLibaoDriver; - if(pLibaoDeviceName != NULL) - libao_devicename = pLibaoDeviceName; - if(pLibaoDeviceId != NULL) - libao_deviceid = pLibaoDeviceId; - - controlport = pCtrlPort; - timingport = pTimingPort; - dataport = pDataPort; - if(bufStartFill < 0) - bufStartFill = START_FILL; - buffer_start_fill = bufStartFill; - - AES_set_decrypt_key(aeskey, 128, &aes); - - memset(fmtp, 0, sizeof(fmtp)); - int i = 0; - char *arg; - while ( (arg = strsep(&fmtpstr, " \t")) ) - fmtp[i++] = atoi(arg); - - init_decoder(); - init_buffer(); - init_rtp(); // open a UDP listen port and start a listener; decode into ring buffer - fflush(stdout); - init_output(); // resample and output from ring buffer - - char line[128]; - int in_line = 0; - int n; - double f; - while (fgets(line + in_line, sizeof(line) - in_line, stdin)) { - n = strlen(line); - if (line[n-1] != '\n') { - in_line = strlen(line) - 1; - if (n == sizeof(line)-1) - in_line = 0; - continue; - } - if (sscanf(line, "vol: %lf\n", &f)) { - assert(f<=0); - if (debug) - fprintf(stderr, "VOL: %lf\n", f); - pthread_mutex_lock(&vol_mutex); - volume = pow(10.0,0.05*f); - fix_volume = 65536.0 * volume; - pthread_mutex_unlock(&vol_mutex); - continue; - } - if (!strcmp(line, "exit\n")) { - exit(0); - } - if (!strcmp(line, "flush\n")) { - pthread_mutex_lock(&ab_mutex); - ab_resync(); - pthread_mutex_unlock(&ab_mutex); - if (debug) - fprintf(stderr, "FLUSH\n"); - } - } - fprintf(stderr, "bye!\n"); - fflush(stderr); - - return EXIT_SUCCESS; -} - -#ifdef AF_INET6 -static int ipv4_only = 0; -#else -static int ipv4_only = 1; -#endif - -#ifdef HAIRTUNES_STANDALONE -int main(int argc, char **argv) { - char *hexaeskey = 0, *hexaesiv = 0; - char *fmtpstr = 0; - char *arg; - assert(RAND_MAX >= 0x10000); // XXX move this to compile time - while ( (arg = *++argv) ) { - if (!strcasecmp(arg, "iv")) { - hexaesiv = *++argv; - argc--; - } else - if (!strcasecmp(arg, "ipv4_only")) { - ipv4_only = 1; - } else - if (!strcasecmp(arg, "key")) { - hexaeskey = *++argv; - argc--; - } else - if (!strcasecmp(arg, "fmtp")) { - fmtpstr = *++argv; - } else - if (!strcasecmp(arg, "cport")) { - controlport = atoi(*++argv); - } else - if (!strcasecmp(arg, "tport")) { - timingport = atoi(*++argv); - } else - if (!strcasecmp(arg, "dport")) { - dataport = atoi(*++argv); - } else - if (!strcasecmp(arg, "host")) { - rtphost = *++argv; - } else - if (!strcasecmp(arg, "pipe")) { - if (libao_driver || libao_devicename || libao_deviceid ) { - die("Option 'pipe' may not be combined with 'ao_driver', 'ao_devicename' or 'ao_deviceid'"); - } - - pipename = *++argv; - } else - if (!strcasecmp(arg, "ao_driver")) { - if (pipename) { - die("Option 'ao_driver' may not be combined with 'pipe'"); - } - - libao_driver = *++argv; - } else - if (!strcasecmp(arg, "ao_devicename")) { - if (pipename || libao_deviceid ) { - die("Option 'ao_devicename' may not be combined with 'pipe' or 'ao_deviceid'"); - } - - libao_devicename = *++argv; - } else - if (!strcasecmp(arg, "ao_deviceid")) { - if (pipename || libao_devicename) { - die("Option 'ao_deviceid' may not be combined with 'pipe' or 'ao_devicename'"); - } - - libao_deviceid = *++argv; - } -#ifdef FANCY_RESAMPLING - else - if (!strcasecmp(arg, "resamp")) { - fancy_resampling = atoi(*++argv); - } -#endif - } - - if (!hexaeskey || !hexaesiv) - die("Must supply AES key and IV!"); - - if (hex2bin(aesiv, hexaesiv)) - die("can't understand IV"); - if (hex2bin(aeskey, hexaeskey)) - die("can't understand key"); - return hairtunes_init(NULL, NULL, fmtpstr, controlport, timingport, dataport, - NULL, NULL, NULL, NULL, NULL, START_FILL); -} -#endif - -static void init_buffer(void) { - int i; - for (i=0; i 0; -} - -static void alac_decode(short *dest, char *buf, int len) { - unsigned char packet[MAX_PACKET]; - assert(len<=MAX_PACKET); - - unsigned char iv[16]; - int aeslen = len & ~0xf; - memcpy(iv, aesiv, sizeof(iv)); - AES_cbc_encrypt((unsigned char*)buf, packet, aeslen, &aes, iv, AES_DECRYPT); - memcpy(packet+aeslen, buf+aeslen, len-aeslen); - - int outsize; - - decode_frame(decoder_info, packet, dest, &outsize); - - assert(outsize == FRAME_BYTES); -} - -static void buffer_put_packet(seq_t seqno, char *data, int len) { - abuf_t *abuf = 0; - short buf_fill; - - pthread_mutex_lock(&ab_mutex); - if (!ab_synced) { - ab_write = seqno; - ab_read = seqno-1; - ab_synced = 1; - } - if (seqno == ab_write+1) { // expected packet - abuf = audio_buffer + BUFIDX(seqno); - ab_write = seqno; - } else if (seq_order(ab_write, seqno)) { // newer than expected - rtp_request_resend(ab_write+1, seqno-1); - abuf = audio_buffer + BUFIDX(seqno); - ab_write = seqno; - } else if (seq_order(ab_read, seqno)) { // late but not yet played - abuf = audio_buffer + BUFIDX(seqno); - } else { // too late. - fprintf(stderr, "\nlate packet %04X (%04X:%04X)\n", seqno, ab_read, ab_write); - } - buf_fill = ab_write - ab_read; - pthread_mutex_unlock(&ab_mutex); - - if (abuf) { - alac_decode(abuf->data, data, len); - abuf->ready = 1; - } - - pthread_mutex_lock(&ab_mutex); - if (ab_buffering && buf_fill >= buffer_start_fill) { - ab_buffering = 0; - pthread_cond_signal(&ab_buffer_ready); - } - pthread_mutex_unlock(&ab_mutex); -} - -static int rtp_sockets[2]; // data, control -#ifdef AF_INET6 -static struct sockaddr_in6 rtp_client6; -#endif -static struct sockaddr_in rtp_client4; - -struct sockaddr * get_rtp_client_sockaddr(void) { - -#ifdef AF_INET6 - if (!ipv4_only) { - return ((struct sockaddr*)&rtp_client6); - } -#endif - return ((struct sockaddr*)&rtp_client4); -} - -socklen_t get_rtp_sockaddr_len(void) { -#ifdef AF_INET6 - if (!ipv4_only) { - return (sizeof(&rtp_client6)); - } -#endif - return (sizeof(rtp_client4)); -} - - - -static void *rtp_thread_func(void *arg) { - socklen_t si_len; - char packet[MAX_PACKET]; - char *pktp; - seq_t seqno; - ssize_t plen; - int sock = rtp_sockets[0], csock = rtp_sockets[1]; - int readsock; - char type; - struct sockaddr *rtp_client; - - rtp_client = get_rtp_client_sockaddr(); - si_len = get_rtp_sockaddr_len(); - - fd_set fds; - FD_ZERO(&fds); - FD_SET(sock, &fds); - FD_SET(csock, &fds); - - while (select(csock>sock ? csock+1 : sock+1, &fds, 0, 0, 0)!=-1) { - if (FD_ISSET(sock, &fds)) { - readsock = sock; - } else { - readsock = csock; - } - FD_SET(sock, &fds); - FD_SET(csock, &fds); - - plen = recvfrom(readsock, packet, sizeof(packet), 0, rtp_client, &si_len); - if (plen < 0) - continue; - assert(plen<=MAX_PACKET); - - type = packet[1] & ~0x80; - if (type == 0x60 || type == 0x56) { // audio data / resend - pktp = packet; - if (type==0x56) { - pktp += 4; - plen -= 4; - } - seqno = ntohs(*(unsigned short *)(pktp+2)); - - // adjust pointer and length - pktp += 12; - plen -= 12; - - // check if packet contains enough content to be reasonable - if (plen >= 16) { - buffer_put_packet(seqno, pktp, plen); - } else { - // resync? - if (type == 0x56 && seqno == 0) { - fprintf(stderr, "Suspected resync request packet received. Initiating resync.\n"); - pthread_mutex_lock(&ab_mutex); - ab_resync(); - pthread_mutex_unlock(&ab_mutex); - } - } - } - } - - return 0; -} - -static void rtp_request_resend(seq_t first, seq_t last) { - if (seq_order(last, first)) - return; - - fprintf(stderr, "requesting resend on %d packets (port %d)\n", last-first+1, controlport); - - char req[8]; // *not* a standard RTCP NACK - req[0] = 0x80; - req[1] = 0x55|0x80; // Apple 'resend' - *(unsigned short *)(req+2) = htons(1); // our seqnum - *(unsigned short *)(req+4) = htons(first); // missed seqnum - *(unsigned short *)(req+6) = htons(last-first+1); // count - - struct sockaddr *rtp_client = get_rtp_client_sockaddr(); - - if (ipv4_only) { - ((struct sockaddr_in *)rtp_client)->sin_port = htons(controlport); - } -#ifdef AF_INET6 - else { - ((struct sockaddr_in6 *)rtp_client)->sin6_port = htons(controlport); - } -#endif - sendto(rtp_sockets[1], req, sizeof(req), 0, rtp_client, get_rtp_sockaddr_len()); -} - - -static int init_rtp(void) { - struct sockaddr_in si; - int type = AF_INET; - struct sockaddr* si_p = (struct sockaddr*)&si; - socklen_t si_len = sizeof(si); - unsigned short *sin_port = &si.sin_port; - memset(&si, 0, sizeof(si)); -#ifdef AF_INET6 - struct sockaddr_in6 si6; - - if (!ipv4_only) { - type = AF_INET6; - si_p = (struct sockaddr*)&si6; - si_len = sizeof(si6); - sin_port = &si6.sin6_port; - memset(&si6, 0, sizeof(si6)); - } -#endif - - si.sin_family = AF_INET; -#ifdef SIN_LEN - si.sin_len = sizeof(si); -#endif - si.sin_addr.s_addr = htonl(INADDR_ANY); -#ifdef AF_INET6 - if (!ipv4_only) { - si6.sin6_family = AF_INET6; - #ifdef SIN6_LEN - si6.sin6_len = sizeof(si); - #endif - si6.sin6_addr = in6addr_any; - si6.sin6_flowinfo = 0; - } -#endif - - int sock = -1, csock = -1; // data and control (we treat the streams the same here) - unsigned short port = 6000; - while(1) { - if(sock < 0) - sock = socket(type, SOCK_DGRAM, IPPROTO_UDP); -#ifdef AF_INET6 - if(sock==-1 && type == AF_INET6) { - // try fallback to IPv4 - type = AF_INET; - si_p = (struct sockaddr*)&si; - si_len = sizeof(si); - sin_port = &si.sin_port; - continue; - } -#endif - if (sock==-1) - die("Can't create data socket!"); - - if(csock < 0) - csock = socket(type, SOCK_DGRAM, IPPROTO_UDP); - if (csock==-1) - die("Can't create control socket!"); - - *sin_port = htons(port); - int bind1 = bind(sock, si_p, si_len); - *sin_port = htons(port + 1); - int bind2 = bind(csock, si_p, si_len); - - if(bind1 != -1 && bind2 != -1) break; - if(bind1 != -1) { close(sock); sock = -1; } - if(bind2 != -1) { close(csock); csock = -1; } - - port += 3; - } - - printf("port: %d\n", port); // let our handler know where we end up listening - printf("cport: %d\n", port+1); - - pthread_t rtp_thread; - rtp_sockets[0] = sock; - rtp_sockets[1] = csock; - pthread_create(&rtp_thread, NULL, rtp_thread_func, (void *)rtp_sockets); - - return port; -} - -static short lcg_rand(void) { - static unsigned long lcg_prev = 12345; - lcg_prev = lcg_prev * 69069 + 3; - return lcg_prev & 0xffff; -} - -static inline short dithered_vol(short sample) { - static short rand_a, rand_b; - long out; - - out = (long)sample * fix_volume; - if (fix_volume < 0x10000) { - rand_b = rand_a; - rand_a = lcg_rand(); - out += rand_a; - out -= rand_b; - } - return out>>16; -} - -typedef struct { - double hist[2]; - double a[2]; - double b[3]; -} biquad_t; - -static void biquad_init(biquad_t *bq, double a[], double b[]) { - bq->hist[0] = bq->hist[1] = 0.0; - memcpy(bq->a, a, 2*sizeof(double)); - memcpy(bq->b, b, 3*sizeof(double)); -} - -static void biquad_lpf(biquad_t *bq, double freq, double Q) { - double w0 = 2.0 * M_PI * freq * frame_size / (double)sampling_rate; - double alpha = sin(w0)/(2.0*Q); - - double a_0 = 1.0 + alpha; - double b[3], a[2]; - b[0] = (1.0-cos(w0))/(2.0*a_0); - b[1] = (1.0-cos(w0))/a_0; - b[2] = b[0]; - a[0] = -2.0*cos(w0)/a_0; - a[1] = (1-alpha)/a_0; - - biquad_init(bq, a, b); -} - -static double biquad_filt(biquad_t *bq, double in) { - double w = in - bq->a[0]*bq->hist[0] - bq->a[1]*bq->hist[1]; - double out = bq->b[1]*bq->hist[0] + bq->b[2]*bq->hist[1] + bq->b[0]*w; - bq->hist[1] = bq->hist[0]; - bq->hist[0] = w; - - return out; -} - -static double bf_playback_rate = 1.0; - -static double bf_est_drift = 0.0; // local clock is slower by -static biquad_t bf_drift_lpf; -static double bf_est_err = 0.0, bf_last_err; -static biquad_t bf_err_lpf, bf_err_deriv_lpf; -static double desired_fill; -static int fill_count; - -static void bf_est_reset(short fill) { - biquad_lpf(&bf_drift_lpf, 1.0/180.0, 0.3); - biquad_lpf(&bf_err_lpf, 1.0/10.0, 0.25); - biquad_lpf(&bf_err_deriv_lpf, 1.0/2.0, 0.2); - fill_count = 0; - bf_playback_rate = 1.0; - bf_est_err = bf_last_err = 0; - desired_fill = fill_count = 0; -} - -static void bf_est_update(short fill) { - if (fill_count < 1000) { - desired_fill += (double)fill/1000.0; - fill_count++; - return; - } - -#define CONTROL_A (1e-4) -#define CONTROL_B (1e-1) - - double buf_delta = fill - desired_fill; - bf_est_err = biquad_filt(&bf_err_lpf, buf_delta); - double err_deriv = biquad_filt(&bf_err_deriv_lpf, bf_est_err - bf_last_err); - double adj_error = CONTROL_A * bf_est_err; - - bf_est_drift = biquad_filt(&bf_drift_lpf, CONTROL_B*(adj_error + err_deriv) + bf_est_drift); - - if (debug) - fprintf(stderr, "bf %d err %f drift %f desiring %f ed %f estd %f\n", - fill, bf_est_err, bf_est_drift, desired_fill, err_deriv, err_deriv + adj_error); - bf_playback_rate = 1.0 + adj_error + bf_est_drift; - - bf_last_err = bf_est_err; -} - -// get the next frame, when available. return 0 if underrun/stream reset. -static short *buffer_get_frame(void) { - short buf_fill; - seq_t read; - abuf_t *abuf = 0; - unsigned short next; - int i; - - pthread_mutex_lock(&ab_mutex); - - buf_fill = ab_write - ab_read; - if (buf_fill < 1 || !ab_synced || ab_buffering) { // init or underrun. stop and wait - if (ab_synced) - fprintf(stderr, "\nunderrun.\n"); - - ab_buffering = 1; - pthread_cond_wait(&ab_buffer_ready, &ab_mutex); - ab_read++; - buf_fill = ab_write - ab_read; - bf_est_reset(buf_fill); - pthread_mutex_unlock(&ab_mutex); - - return 0; - } - if (buf_fill >= BUFFER_FRAMES) { // overrunning! uh-oh. restart at a sane distance - fprintf(stderr, "\noverrun.\n"); - ab_read = ab_write - buffer_start_fill; - } - read = ab_read; - ab_read++; - buf_fill = ab_write - ab_read; - bf_est_update(buf_fill); - - // check if t+16, t+32, t+64, t+128, ... (START_FILL / 2) - // packets have arrived... last-chance resend - if (!ab_buffering) { - for (i = 16; i < (START_FILL / 2); i = (i * 2)) { - next = ab_read + i; - abuf = audio_buffer + BUFIDX(next); - if (!abuf->ready) { - rtp_request_resend(next, next); - } - } - } - - abuf_t *curframe = audio_buffer + BUFIDX(read); - if (!curframe->ready) { - fprintf(stderr, "\nmissing frame.\n"); - memset(curframe->data, 0, FRAME_BYTES); - } - curframe->ready = 0; - pthread_mutex_unlock(&ab_mutex); - - return curframe->data; -} - -static int stuff_buffer(double playback_rate, short *inptr, short *outptr) { - int i; - int stuffsamp = frame_size; - int stuff = 0; - double p_stuff; - - p_stuff = 1.0 - pow(1.0 - fabs(playback_rate-1.0), frame_size); - - if (rand() < p_stuff * RAND_MAX) { - stuff = playback_rate > 1.0 ? -1 : 1; - stuffsamp = rand() % (frame_size - 1); - } - - pthread_mutex_lock(&vol_mutex); - for (i=0; i> 1); - *outptr++ = dithered_vol(((long)inptr[-1] + (long)inptr[1]) >> 1); - } else if (stuff==-1) { - if (debug) - fprintf(stderr, "---------\n"); - inptr++; - inptr++; - } - for (i=stuffsamp; i - - - - Label - org.mafipulation.shairport - RunAtLoad - - ProgramArguments - - /usr/local/bin/shairport.pl - --apname - RJ - - - diff --git a/player.c b/player.c new file mode 100644 index 00000000..a5bd8895 --- /dev/null +++ b/player.c @@ -0,0 +1,478 @@ +// slave-clocked stream player + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "player.h" +#include "rtp.h" + +#ifdef FANCY_RESAMPLING +#include +#endif + +#include "alac.h" + +// parameters from the source +static unsigned char *aesiv; +static AES_KEY aes; +static int sampling_rate, frame_size; + +#define FRAME_BYTES (4*frame_size) +// maximal resampling shift - conservative +#define OUTFRAME_BYTES (4*(frame_size+3)) + +static pthread_t player_thread; +static int please_stop; + +static alac_file *decoder_info; + +#ifdef FANCY_RESAMPLING +static int fancy_resampling = 1; +static SRC_STATE *src; +#endif + + +// interthread variables +static double volume = 1.0; +static int fix_volume = 0x10000; +static pthread_mutex_t vol_mutex = PTHREAD_MUTEX_INITIALIZER; + +// default buffer size +// needs to be a power of 2 because of the way BUFIDX(seqno) works +#define BUFFER_FRAMES 512 +#define START_FILL 250 +#define MAX_PACKET 2048 + +typedef struct audio_buffer_entry { // decoded audio packets + int ready; + signed short *data; +} abuf_t; +static abuf_t audio_buffer[BUFFER_FRAMES]; +#define BUFIDX(seqno) ((seq_t)(seqno) % BUFFER_FRAMES) + +// mutex-protected variables +static seq_t ab_read, ab_write; +static int ab_buffering = 1, ab_synced = 0; +static pthread_mutex_t ab_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t ab_buffer_ready = PTHREAD_COND_INITIALIZER; + +static void ab_resync(void) { + int i; + for (i=0; i 0; +} + +static void alac_decode(short *dest, uint8_t *buf, int len) { + unsigned char packet[MAX_PACKET]; + assert(len<=MAX_PACKET); + + unsigned char iv[16]; + int aeslen = len & ~0xf; + memcpy(iv, aesiv, sizeof(iv)); + AES_cbc_encrypt(buf, packet, aeslen, &aes, iv, AES_DECRYPT); + memcpy(packet+aeslen, buf+aeslen, len-aeslen); + + int outsize; + + alac_decode_frame(decoder_info, packet, dest, &outsize); + + assert(outsize == FRAME_BYTES); +} + + +static int init_decoder(int32_t fmtp[12]) { + alac_file *alac; + + frame_size = fmtp[1]; // stereo samples + sampling_rate = fmtp[11]; + + int sample_size = fmtp[3]; + if (sample_size != 16) + die("only 16-bit samples supported!"); + + alac = alac_create(sample_size, 2); + if (!alac) + return 1; + decoder_info = alac; + + alac->setinfo_max_samples_per_frame = frame_size; + alac->setinfo_7a = fmtp[2]; + alac->setinfo_sample_size = sample_size; + alac->setinfo_rice_historymult = fmtp[4]; + alac->setinfo_rice_initialhistory = fmtp[5]; + alac->setinfo_rice_kmodifier = fmtp[6]; + alac->setinfo_7f = fmtp[7]; + alac->setinfo_80 = fmtp[8]; + alac->setinfo_82 = fmtp[9]; + alac->setinfo_86 = fmtp[10]; + alac->setinfo_8a_rate = fmtp[11]; + alac_allocate_buffers(alac); + return 0; +} + +#ifdef FANCY_RESAMPLING +static int init_src(void) { + int err; + if (fancy_resampling) + src = src_new(SRC_SINC_MEDIUM_QUALITY, 2, &err); + else + src = 0; + + return err; +} +#endif + +static void init_buffer(void) { + int i; + for (i=0; idata, data, len); + abuf->ready = 1; + } + + pthread_mutex_lock(&ab_mutex); + if (ab_buffering && buf_fill >= config.buffer_start_fill) { + debug("buffering over. starting play\n"); + ab_buffering = 0; + pthread_cond_signal(&ab_buffer_ready); + } + pthread_mutex_unlock(&ab_mutex); +} + + +static short lcg_rand(void) { + static unsigned long lcg_prev = 12345; + lcg_prev = lcg_prev * 69069 + 3; + return lcg_prev & 0xffff; +} + +static inline short dithered_vol(short sample) { + static short rand_a, rand_b; + long out; + + out = (long)sample * fix_volume; + if (fix_volume < 0x10000) { + rand_b = rand_a; + rand_a = lcg_rand(); + out += rand_a; + out -= rand_b; + } + return out>>16; +} + +typedef struct { + double hist[2]; + double a[2]; + double b[3]; +} biquad_t; + +static void biquad_init(biquad_t *bq, double a[], double b[]) { + bq->hist[0] = bq->hist[1] = 0.0; + memcpy(bq->a, a, 2*sizeof(double)); + memcpy(bq->b, b, 3*sizeof(double)); +} + +static void biquad_lpf(biquad_t *bq, double freq, double Q) { + double w0 = 2.0 * M_PI * freq * frame_size / (double)sampling_rate; + double alpha = sin(w0)/(2.0*Q); + + double a_0 = 1.0 + alpha; + double b[3], a[2]; + b[0] = (1.0-cos(w0))/(2.0*a_0); + b[1] = (1.0-cos(w0))/a_0; + b[2] = b[0]; + a[0] = -2.0*cos(w0)/a_0; + a[1] = (1-alpha)/a_0; + + biquad_init(bq, a, b); +} + +static double biquad_filt(biquad_t *bq, double in) { + double w = in - bq->a[0]*bq->hist[0] - bq->a[1]*bq->hist[1]; + double out = bq->b[1]*bq->hist[0] + bq->b[2]*bq->hist[1] + bq->b[0]*w; + bq->hist[1] = bq->hist[0]; + bq->hist[0] = w; + + return out; +} + +static double bf_playback_rate = 1.0; + +static double bf_est_drift = 0.0; // local clock is slower by +static biquad_t bf_drift_lpf; +static double bf_est_err = 0.0, bf_last_err; +static biquad_t bf_err_lpf, bf_err_deriv_lpf; +static double desired_fill; +static int fill_count; + +static void bf_est_reset(short fill) { + biquad_lpf(&bf_drift_lpf, 1.0/180.0, 0.3); + biquad_lpf(&bf_err_lpf, 1.0/10.0, 0.25); + biquad_lpf(&bf_err_deriv_lpf, 1.0/2.0, 0.2); + fill_count = 0; + bf_playback_rate = 1.0; + bf_est_err = bf_last_err = 0; + desired_fill = fill_count = 0; +} + +static void bf_est_update(short fill) { + if (fill_count < 1000) { + desired_fill += (double)fill/1000.0; + fill_count++; + return; + } + +#define CONTROL_A (1e-4) +#define CONTROL_B (1e-1) + + double buf_delta = fill - desired_fill; + bf_est_err = biquad_filt(&bf_err_lpf, buf_delta); + double err_deriv = biquad_filt(&bf_err_deriv_lpf, bf_est_err - bf_last_err); + double adj_error = CONTROL_A * bf_est_err; + + bf_est_drift = biquad_filt(&bf_drift_lpf, CONTROL_B*(adj_error + err_deriv) + bf_est_drift); + + debug("bf %d err %f drift %f desiring %f ed %f estd %f\n", + fill, bf_est_err, bf_est_drift, desired_fill, err_deriv, err_deriv + adj_error); + bf_playback_rate = 1.0 + adj_error + bf_est_drift; + + bf_last_err = bf_est_err; +} + +// get the next frame, when available. return 0 if underrun/stream reset. +static short *buffer_get_frame(void) { + short buf_fill; + seq_t read; + abuf_t *abuf = 0; + unsigned short next; + int i; + + pthread_mutex_lock(&ab_mutex); + + buf_fill = ab_write - ab_read; + if (buf_fill < 1 || !ab_synced || ab_buffering) { // init or underrun. stop and wait + if (ab_synced) + warn("underrun.\n"); + + ab_buffering = 1; + pthread_cond_wait(&ab_buffer_ready, &ab_mutex); + ab_read++; + buf_fill = ab_write - ab_read; + bf_est_reset(buf_fill); + pthread_mutex_unlock(&ab_mutex); + + return 0; + } + if (buf_fill >= BUFFER_FRAMES) { // overrunning! uh-oh. restart at a sane distance + warn("overrun.\n"); + ab_read = ab_write - config.buffer_start_fill; + } + read = ab_read; + ab_read++; + buf_fill = ab_write - ab_read; + bf_est_update(buf_fill); + + // check if t+16, t+32, t+64, t+128, ... (START_FILL / 2) + // packets have arrived... last-chance resend + if (!ab_buffering) { + for (i = 16; i < (START_FILL / 2); i = (i * 2)) { + next = ab_read + i; + abuf = audio_buffer + BUFIDX(next); + if (!abuf->ready) { + rtp_request_resend(next, next); + } + } + } + + abuf_t *curframe = audio_buffer + BUFIDX(read); + if (!curframe->ready) { + warn("missing frame.\n"); + memset(curframe->data, 0, FRAME_BYTES); + } + curframe->ready = 0; + pthread_mutex_unlock(&ab_mutex); + + return curframe->data; +} + +static int stuff_buffer(double playback_rate, short *inptr, short *outptr) { + int i; + int stuffsamp = frame_size; + int stuff = 0; + double p_stuff; + + p_stuff = 1.0 - pow(1.0 - fabs(playback_rate-1.0), frame_size); + + if (rand() < p_stuff * RAND_MAX) { + stuff = playback_rate > 1.0 ? -1 : 1; + stuffsamp = rand() % (frame_size - 1); + } + + pthread_mutex_lock(&vol_mutex); + for (i=0; i> 1); + *outptr++ = dithered_vol(((long)inptr[-1] + (long)inptr[1]) >> 1); + } else if (stuff==-1) { + debug("---------\n"); + inptr++; + inptr++; + } + for (i=stuffsamp; iplay(outbuf, play_samples); + } + + return 0; +} + +// takes the volume as specified by the airplay protocol +void player_volume(double f) { + double linear_volume = pow(10.0, 0.05*f); + + if (config.output->volume) { + config.output->volume(linear_volume); + } else { + pthread_mutex_lock(&vol_mutex); + volume = linear_volume; + fix_volume = 65536.0 * volume; + pthread_mutex_unlock(&vol_mutex); + } +} +void player_flush(void) { + pthread_mutex_lock(&ab_mutex); + ab_resync(); + pthread_mutex_unlock(&ab_mutex); +} + +int player_play(stream_cfg *stream) { + AES_set_decrypt_key(stream->aeskey, 128, &aes); + aesiv = stream->aesiv; + init_decoder(stream->fmtp); + init_buffer(); +#ifdef FANCY_RESAMPLING + init_src(); +#endif + + please_stop = 0; + config.output->start(sampling_rate); + pthread_create(&player_thread, NULL, player_thread_func, NULL); + + return 0; +} + +void player_stop(void) { + please_stop = 1; + pthread_join(player_thread, NULL); + config.output->stop(); +} diff --git a/player.h b/player.h new file mode 100644 index 00000000..9222df7a --- /dev/null +++ b/player.h @@ -0,0 +1,22 @@ +#ifndef _PLAYER_H +#define _PLAYER_H + +#include "audio.h" + +typedef struct { + uint8_t aesiv[16], aeskey[16]; + int32_t fmtp[12]; +} stream_cfg; + +typedef unsigned short seq_t; + +int player_play(stream_cfg *cfg); +void player_stop(void); + +void player_volume(double f); +void player_flush(void); +void player_resync(void); + +void player_put_packet(seq_t seqno, uint8_t *data, int len); + +#endif //_PLAYER_H diff --git a/rtp.c b/rtp.c new file mode 100644 index 00000000..e90692ed --- /dev/null +++ b/rtp.c @@ -0,0 +1,157 @@ +// only one RTP session can be active at a time. +#include +#include +#include +#include +#include +#include +#include "common.h" +#include "player.h" + +static int running = 0; +static int please_shutdown; + +static struct sockaddr rtp_client; +static int sock; +static pthread_t rtp_thread; + +static void *rtp_receiver(void *arg) { + // we inherit the signal mask (SIGUSR1) + uint8_t packet[2048], *pktp; + + ssize_t nread; + while (1) { + if (please_shutdown) + break; + nread = recv(sock, packet, sizeof(packet), 0); + if (nread < 0) + break; + + ssize_t plen = nread; + uint8_t type = packet[1] & ~0x80; + if (type == 0x60 || type == 0x56) { // audio data / resend + pktp = packet; + if (type==0x56) { + pktp += 4; + plen -= 4; + } + seq_t seqno = ntohs(*(unsigned short *)(pktp+2)); + + pktp += 12; + plen -= 12; + + // check if packet contains enough content to be reasonable + if (plen >= 16) { + player_put_packet(seqno, pktp, plen); + continue; + } + if (type == 0x56 && seqno == 0) { + debug("Suspected resync request packet received.\n"); + player_flush(); + continue; + } + } + warn("Unknown RTP packet of type 0x%02X length %d\n", type, nread); + } + + debug("RTP thread interrupted. terminating.\n"); + close(sock); + + return NULL; +} + +static int bind_port(struct sockaddr *remote) { + struct addrinfo hints, *info; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = remote->sa_family; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_PASSIVE; + + int ret = getaddrinfo(NULL, "0", &hints, &info); + + if (ret < 0) + die("failed to get usable addrinfo?! %s", gai_strerror(ret)); + + sock = socket(remote->sa_family, SOCK_DGRAM, IPPROTO_UDP); + ret = bind(sock, info->ai_addr, info->ai_addrlen); + + if (ret < 0) + die("could not bind a UDP port!"); + + int sport; + struct sockaddr local; + socklen_t local_len = sizeof(local); + getsockname(sock, &local, &local_len); +#ifdef AF_INET6 + if (local.sa_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)&local; + sport = htons(sa6->sin6_port); + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in*)&local; + sport = htons(sa->sin_port); + } + + return sport; +} + + +int rtp_setup(struct sockaddr *remote, int cport, int tport) { + if (running) + die("rtp_setup called with active stream!\n"); + + debug("rtp_setup: cport=%d tport=%d\n", cport, tport); + + // we do our own timing and ignore the timing port. + // an audio perfectionist may wish to learn the protocol. + + memcpy(&rtp_client, remote, sizeof(rtp_client)); +#ifdef AF_INET6 + if (rtp_client.sa_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)&rtp_client; + sa6->sin6_port = htons(cport); + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in*)&rtp_client; + sa->sin_port = htons(cport); + } + + int sport = bind_port(remote); + + debug("rtp listening on port %d\n", sport); + + please_shutdown = 0; + pthread_create(&rtp_thread, NULL, &rtp_receiver, NULL); + + running = 1; + return sport; +} + +void rtp_shutdown(void) { + if (!running) + die("rtp_shutdown called without active stream!\n"); + + please_shutdown = 1; + pthread_kill(rtp_thread, SIGUSR1); + void *retval; + pthread_join(rtp_thread, &retval); + running = 0; +} + +void rtp_request_resend(seq_t first, seq_t last) { + if (!running) + die("rtp_request_resend called without active stream!\n"); + + warn("requesting resend on %d packets\n", last-first+1); + char req[8]; // *not* a standard RTCP NACK + req[0] = 0x80; + req[1] = 0x55|0x80; // Apple 'resend' + *(unsigned short *)(req+2) = htons(1); // our seqnum + *(unsigned short *)(req+4) = htons(first); // missed seqnum + *(unsigned short *)(req+6) = htons(last-first+1); // count + + sendto(sock, req, sizeof(req), 0, &rtp_client, sizeof(rtp_client)); +} diff --git a/rtp.h b/rtp.h new file mode 100644 index 00000000..91a23b12 --- /dev/null +++ b/rtp.h @@ -0,0 +1,10 @@ +#ifndef _RTP_H +#define _RTP_H + +#include + +int rtp_setup(struct sockaddr *remote, int controlport, int timingport); +void rtp_shutdown(void); +void rtp_request_resend(seq_t first, seq_t last); + +#endif // _RTP_H diff --git a/rtsp.c b/rtsp.c new file mode 100644 index 00000000..f4654c6c --- /dev/null +++ b/rtsp.c @@ -0,0 +1,723 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "player.h" +#include "rtp.h" + +// only one thread is allowed to use the player at once. +// it monitors the request variable (at least when interrupted) +static pthread_mutex_t playing_mutex = PTHREAD_MUTEX_INITIALIZER; +static int please_shutdown = 0; +static pthread_t playing_thread = 0; + +typedef struct { + stream_cfg stream; + struct sockaddr remote; +} rtsp_conn_info; + +static inline int rtsp_playing(void) { + return playing_thread == pthread_self(); +} + +static void rtsp_take_player(void) { + if (pthread_mutex_trylock(&playing_mutex)) { + debug("another thread is playing, shutting it down\n"); + // XXX minor race condition between please_shutdown and signal delivery + please_shutdown = 1; + pthread_kill(playing_thread, SIGUSR1); + pthread_mutex_lock(&playing_mutex); + } +} + +void rtsp_shutdown_stream(void) { + rtsp_take_player(); + pthread_mutex_unlock(&playing_mutex); +} + + +// park a null at the line ending, and return the next line pointer +// accept \r, \n, or \r\n +static char *nextline(char *in, int inbuf) { + char *out = NULL; + while (inbuf) { + if (*in == '\r') { + *in++ = 0; + out = in; + } + if (*in == '\n') { + *in++ = 0; + out = in; + } + + if (out) + break; + + in++; + inbuf--; + } + return out; +} + +typedef struct { + int nheaders; + char *name[16]; + char *value[16]; + + int contentlength; + char *content; + + // for requests + char method[16]; + + // for responses + int respcode; +} rtsp_message; + +static rtsp_message * msg_init(void) { + rtsp_message *msg = malloc(sizeof(rtsp_message)); + memset(msg, 0, sizeof(rtsp_message)); + return msg; +} + +static int msg_add_header(rtsp_message *msg, char *name, char *value) { + if (msg->nheaders >= sizeof(msg->name)/sizeof(char*)) { + warn("too many headers?!\n"); + return 1; + } + + msg->name[msg->nheaders] = strdup(name); + msg->value[msg->nheaders] = strdup(value); + msg->nheaders++; + + return 0; +} + +static char *msg_get_header(rtsp_message *msg, char *name) { + int i; + for (i=0; inheaders; i++) + if (!strcasecmp(msg->name[i], name)) + return msg->value[i]; + return NULL; +} + +static void msg_free(rtsp_message *msg) { + int i; + for (i=0; inheaders; i++) { + free(msg->name[i]); + free(msg->value[i]); + } + if (msg->content) + free(msg->content); + free(msg); +} + + +static int msg_handle_line(rtsp_message **pmsg, char *line) { + rtsp_message *msg = *pmsg; + + if (!msg) { + msg = msg_init(); + *pmsg = msg; + char *sp, *p; + + debug("received request: %s\n", line); + + p = strtok_r(line, " ", &sp); + if (!p) + goto fail; + strncpy(msg->method, p, sizeof(msg->method)-1); + + p = strtok_r(NULL, " ", &sp); + if (!p) + goto fail; + + p = strtok_r(NULL, " ", &sp); + if (!p) + goto fail; + if (strcmp(p, "RTSP/1.0")) + goto fail; + + return -1; + } + + if (strlen(line)) { + char *p; + p = strstr(line, ": "); + if (!p) { + warn("bad header: >>%s<<\n", line); + goto fail; + } + *p = 0; + p += 2; + msg_add_header(msg, line, p); + debug(" %s: %s\n", line, p); + return -1; + } else { + char *cl = msg_get_header(msg, "Content-Length"); + if (cl) + return atoi(cl); + else + return 0; + } + +fail: + *pmsg = NULL; + msg_free(msg); + return 0; +} + +static rtsp_message * rtsp_read_request(int fd) { + ssize_t buflen = 512; + char *buf = malloc(buflen+1); + + rtsp_message *msg = NULL; + + ssize_t nread; + ssize_t inbuf = 0; + int msg_size = -1; + + while (msg_size < 0) { + if (please_shutdown) { + debug("RTSP shutdown requested\n"); + goto shutdown; + } + nread = read(fd, buf+inbuf, buflen - inbuf); + if (!nread) { + debug("RTSP connection closed\n"); + goto shutdown; + } + if (nread < 0) { + if (errno==EINTR) + continue; + perror("read failure"); + goto shutdown; + } + inbuf += nread; + + char *next; + while (msg_size < 0 && (next = nextline(buf, inbuf))) { + msg_size = msg_handle_line(&msg, buf); + + if (!msg) { + warn("no RTSP header received\n"); + goto shutdown; + } + + inbuf -= next-buf; + if (inbuf) + memmove(buf, next, inbuf); + } + } + + if (msg_size > buflen) { + buf = realloc(buf, msg_size); + if (!buf) { + warn("too much content"); + goto shutdown; + } + buflen = msg_size; + } + + while (inbuf < msg_size) { + nread = read(fd, buf+inbuf, msg_size-inbuf); + if (!nread) + goto shutdown; + if (nread==EINTR) + continue; + if (nread < 0) { + perror("read failure"); + goto shutdown; + } + inbuf += nread; + } + + msg->contentlength = inbuf; + msg->content = buf; + return msg; + +shutdown: + free(buf); + if (msg) { + msg_free(msg); + } + return NULL; +} + +static void msg_write_response(int fd, rtsp_message *resp) { + char rbuf[30]; + int nrbuf; + nrbuf = snprintf(rbuf, sizeof(rbuf), + "RTSP/1.0 %d %s\r\n", resp->respcode, + resp->respcode==200 ? "OK" : "Error"); + write(fd, rbuf, nrbuf); + debug("sending response: %s", rbuf); + int i; + for (i=0; inheaders; i++) { + debug(" %s: %s\n", resp->name[i], resp->value[i]); + write(fd, resp->name[i], strlen(resp->name[i])); + write(fd, ": ", 2); + write(fd, resp->value[i], strlen(resp->value[i])); + write(fd, "\r\n", 2); + } + write(fd, "\r\n", 2); +} + +static void handle_options(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + resp->respcode = 200; + msg_add_header(resp, "Public", + "ANNOUNCE, SETUP, RECORD, " + "PAUSE, FLUSH, TEARDOWN, " + "OPTIONS, GET_PARAMETER, SET_PARAMETER"); +} + +static void handle_teardown(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + if (!rtsp_playing()) + return; + resp->respcode = 200; + msg_add_header(resp, "Connection", "close"); + please_shutdown = 1; +} + +static void handle_flush(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + if (!rtsp_playing()) + return; + player_flush(); + resp->respcode = 200; +} + +static void handle_setup(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + playing_thread = pthread_self(); + + int cport, tport; + char *hdr = msg_get_header(req, "Transport"); + if (!hdr) + return; + + char *p; + p = strstr(hdr, "control_port="); + if (!p) + return; + p = strchr(p, '=') + 1; + cport = atoi(p); + + p = strstr(hdr, "timing_port="); + if (!p) + return; + p = strchr(p, '=') + 1; + tport = atoi(p); + + rtsp_take_player(); + int sport = rtp_setup(&conn->remote, cport, tport); + if (!sport) + return; + + player_play(&conn->stream); + + char *resphdr = malloc(strlen(hdr) + 20); + strcpy(resphdr, hdr); + sprintf(resphdr + strlen(resphdr), ";server_port=%d", sport); + msg_add_header(resp, "Transport", resphdr); + + resp->respcode = 200; +} + +static void handle_ignore(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + resp->respcode = 200; +} + +static void handle_announce(rtsp_conn_info *conn, + rtsp_message *req, rtsp_message *resp) { + + char *paesiv = NULL; + char *prsaaeskey = NULL; + char *pfmtp = NULL; + char *cp = req->content; + int cp_left = req->contentlength; + char *next; + while (cp) { + next = nextline(cp, cp_left); + cp_left -= next-cp; + + if (!strncmp(cp, "a=fmtp:", 7)) + pfmtp = cp+7; + + if (!strncmp(cp, "a=aesiv:", 8)) + paesiv = cp+8; + + if (!strncmp(cp, "a=rsaaeskey:", 12)) + prsaaeskey = cp+12; + + cp = next; + } + + if (!paesiv || !prsaaeskey || !pfmtp) { + warn("required params missing from announce\n"); + return; + } + + int len, keylen; + uint8_t *aesiv = base64_dec(paesiv, &len); + if (len != 16) { + warn("client announced aeskey of %d bytes, wanted 16\n", len); + free(aesiv); + return; + } + memcpy(conn->stream.aesiv, aesiv, 16); + free(aesiv); + + uint8_t *rsaaeskey = base64_dec(prsaaeskey, &len); + uint8_t *aeskey = rsa_apply(rsaaeskey, len, &keylen, RSA_MODE_KEY); + free(rsaaeskey); + if (keylen != 16) { + warn("client announced rsaaeskey of %d bytes, wanted 16\n", keylen); + free(aeskey); + return; + } + memcpy(conn->stream.aeskey, aeskey, 16); + free(aeskey); + + int i; + for (i=0; istream.fmtp)/sizeof(conn->stream.fmtp[0]); i++) + conn->stream.fmtp[i] = atoi(strsep(&pfmtp, " \t")); + + resp->respcode = 200; +} + + +static struct method_handler { + char *method; + void (*handler)(rtsp_conn_info *conn, rtsp_message *req, + rtsp_message *resp); +} method_handlers[] = { + {"OPTIONS", handle_options}, + {"ANNOUNCE", handle_announce}, + {"FLUSH", handle_flush}, + {"TEARDOWN", handle_teardown}, + {"SETUP", handle_setup}, + {"GET_PARAMETER", handle_ignore}, + {"SET_PARAMETER", handle_ignore}, // XXX + {"RECORD", handle_ignore}, + {NULL, NULL} +}; + +static void apple_challenge(int fd, rtsp_message *req, rtsp_message *resp) { + char *hdr = msg_get_header(req, "Apple-Challenge"); + if (!hdr) + return; + + struct sockaddr fdsa; + socklen_t sa_len = sizeof(fdsa); + getsockname(fd, &fdsa, &sa_len); + + int chall_len; + uint8_t *chall = base64_dec(hdr, &chall_len); + uint8_t buf[48], *bp = buf; + int i; + memset(buf, 0, sizeof(buf)); + + if (chall_len > 16) { + warn("oversized Apple-Challenge!\n"); + free(chall); + return; + } + memcpy(bp, chall, chall_len); + free(chall); + bp += chall_len; + +#ifdef AF_INET6 + if (fdsa.sa_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6*)(&fdsa); + memcpy(bp, sa6->sin6_addr.s6_addr, 16); + bp += 16; + } else +#endif + { + struct sockaddr_in *sa = (struct sockaddr_in*)(&fdsa); + unsigned int ip = sa->sin_addr.s_addr; + for (i=0; i<4; i++) { + *bp++ = ip & 0xff; + ip >>= 8; + } + } + + for (i=0; i<6; i++) + *bp++ = config.hw_addr[i]; + + int buflen, resplen; + buflen = bp-buf; + if (buflen < 0x20) + buflen = 0x20; + + uint8_t *challresp = rsa_apply(buf, buflen, &resplen, RSA_MODE_AUTH); + char *encoded = base64_enc(challresp, resplen); + + // strip the padding. + char *padding = strchr(encoded, '='); + if (padding) + *padding = 0; + + msg_add_header(resp, "Apple-Response", encoded); + free(challresp); + free(encoded); +} + +static char *make_nonce(void) { + uint8_t random[8]; + int fd = open("/dev/random", O_RDONLY); + if (fd < 0) + die("could not open /dev/random!"); + read(fd, random, sizeof(random)); + close(fd); + return base64_enc(random, 8); +} + +static int rtsp_auth(char **nonce, rtsp_message *req, rtsp_message *resp) { + + if (!config.password) + return 0; + if (!*nonce) { + *nonce = make_nonce(); + goto authenticate; + } + + char *hdr = msg_get_header(req, "Authorization"); + if (!hdr || strncmp(hdr, "Digest ", 7)) + goto authenticate; + + char *realm = strstr(hdr, "realm=\""); + char *username = strstr(hdr, "username=\""); + char *response = strstr(hdr, "response=\""); + char *uri = strstr(hdr, "uri=\""); + + if (!realm || !username || !response || !uri) + goto authenticate; + + char *quote; + realm = strchr(realm, '"') + 1; + if (!(quote = strchr(realm, '"'))) + goto authenticate; + *quote = 0; + username = strchr(username, '"') + 1; + if (!(quote = strchr(username, '"'))) + goto authenticate; + *quote = 0; + response = strchr(response, '"') + 1; + if (!(quote = strchr(response, '"'))) + goto authenticate; + *quote = 0; + uri = strchr(uri, '"') + 1; + if (!(quote = strchr(uri, '"'))) + goto authenticate; + *quote = 0; + + uint8_t digest_urp[16], digest_mu[16], digest_total[16]; + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, username, strlen(username)); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, realm, strlen(realm)); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, config.password, strlen(config.password)); + MD5_Final(digest_urp, &ctx); + + MD5_Init(&ctx); + MD5_Update(&ctx, req->method, strlen(req->method)); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, uri, strlen(uri)); + MD5_Final(digest_mu, &ctx); + + int i; + char buf[33]; + for (i=0; i<16; i++) + sprintf(buf + 2*i, "%02X", digest_urp[i]); + MD5_Init(&ctx); + MD5_Update(&ctx, buf, 32); + MD5_Update(&ctx, ":", 1); + MD5_Update(&ctx, *nonce, strlen(*nonce)); + MD5_Update(&ctx, ":", 1); + for (i=0; i<16; i++) + sprintf(buf + 2*i, "%02X", digest_mu[i]); + MD5_Update(&ctx, buf, 32); + MD5_Final(digest_total, &ctx); + + for (i=0; i<16; i++) + sprintf(buf + 2*i, "%02X", digest_total[i]); + + if (!strcmp(response, buf)) + return 0; + warn("auth failed\n"); + +authenticate: + resp->respcode = 401; + int hdrlen = strlen(*nonce) + 40; + char *authhdr = malloc(hdrlen); + snprintf(authhdr, hdrlen, "Digest realm=\"taco\", nonce=\"%s\"", *nonce); + msg_add_header(resp, "WWW-Authenticate", authhdr); + free(authhdr); + return 1; +} + +static void *rtsp_conversation_thread_func(void *vfd) { + // SIGUSR1 is used to interrupt this thread if blocked for read + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGUSR1); + pthread_sigmask(SIG_UNBLOCK, &set, NULL); + + rtsp_conn_info conn; + memset(&conn, 0, sizeof(conn)); + + int fd = *(int*)vfd; + socklen_t slen = sizeof(conn.remote); + + fd = accept(fd, &conn.remote, &slen); + if (fd < 0) { + perror("failed to accept connection"); + goto shutdown; + } + + rtsp_message *req, *resp; + char *hdr, *auth_nonce = NULL; + while ((req = rtsp_read_request(fd))) { + resp = msg_init(); + resp->respcode = 400; + + apple_challenge(fd, req, resp); + hdr = msg_get_header(req, "CSeq"); + if (hdr) + msg_add_header(resp, "CSeq", hdr); + msg_add_header(resp, "Audio-Jack-Status", "connected; type=analog"); + + if (rtsp_auth(&auth_nonce, req, resp)) + goto respond; + + struct method_handler *mh; + for (mh=method_handlers; mh->method; mh++) { + if (!strcmp(mh->method, req->method)) { + mh->handler(&conn, req, resp); + break; + } + } + +respond: + msg_write_response(fd, resp); + msg_free(req); + msg_free(resp); + } + +shutdown: + if (fd > 0) + close(fd); + if (rtsp_playing()) { + rtp_shutdown(); + player_stop(); + please_shutdown = 0; + pthread_mutex_unlock(&playing_mutex); + } + if (auth_nonce) + free(auth_nonce); + return NULL; +} + +void rtsp_listen_loop(void) { + struct addrinfo hints, *info, *p; + char portstr[6]; + int sockfd[2]; + int nsock = 0; + int ret; + memset(sockfd, 0, sizeof(sockfd)); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + snprintf(portstr, 6, "%d", config.port); + + ret = getaddrinfo(NULL, portstr, &hints, &info); + if (ret) { + die("getaddrinfo failed: %s\n", gai_strerror(ret)); + } + + for (p=info; p; p=p->ai_next) { + int fd = socket(p->ai_family, p->ai_socktype, IPPROTO_TCP); + int yes = 1; + + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + +#ifdef AF_INET6 + // some systems don't support v4 access on v6 sockets, but some do. + // since we need to account for two sockets we might as well always. + if (p->ai_family == AF_INET6) + ret |= setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)); +#endif + + if (!ret) + ret = bind(fd, p->ai_addr, p->ai_addrlen); + + if (ret) { + perror("could not bind a listen socket"); + continue; + } + + listen(fd, 5); + sockfd[nsock++] = fd; + } + + freeaddrinfo(info); + + if (!nsock) + die("could not bind any listen sockets!"); + + + int maxfd = sockfd[0]; + if (sockfd[1]>maxfd) + maxfd = sockfd[1]; + + fd_set fds; + FD_ZERO(&fds); + FD_SET(sockfd[0], &fds); + if (sockfd[1]) + FD_SET(sockfd[1], &fds); + + printf("Listening for connections.\n"); + + int acceptfd; + while (select(maxfd+1, &fds, 0, 0, 0) >= 0) { + if (FD_ISSET(sockfd[0], &fds)) + acceptfd = sockfd[0]; + if (FD_ISSET(sockfd[1], &fds) && sockfd[1]) + acceptfd = sockfd[1]; + + // for now, we do not track these and let them die of natural causes. + // XXX: this leaks threads; they need to be culled with pthread_tryjoin_np. + // XXX: acceptfd could change before the thread is up. which should never happen, but still. + pthread_t rtsp_conversation_thread; + pthread_create(&rtsp_conversation_thread, NULL, rtsp_conversation_thread_func, &acceptfd); + + FD_SET(sockfd[0], &fds); + if (sockfd[1]) + FD_SET(sockfd[1], &fds); + } + perror("select"); + die("fell out of the RTSP select loop\n"); +} diff --git a/rtsp.h b/rtsp.h new file mode 100644 index 00000000..1e61f25d --- /dev/null +++ b/rtsp.h @@ -0,0 +1,7 @@ +#ifndef _RTSP_H +#define _RTSP_H + +void rtsp_listen_loop(void); +void rtsp_shutdown_stream(void); + +#endif // _RTSP_H diff --git a/shairport.c b/shairport.c index 996ee241..69a953ba 100644 --- a/shairport.c +++ b/shairport.c @@ -1,1187 +1,57 @@ -/* - * ShairPort Initializer - Network Initializer/Mgr for Hairtunes RAOP - * Copyright (c) M. Andrew Webster 2011 - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - -#include #include -#include "socketlib.h" -#include "shairport.h" -#include "hairtunes.h" - -#ifndef TRUE -#define TRUE (-1) -#endif -#ifndef FALSE -#define FALSE (0) -#endif - -// TEMP - -int kCurrentLogLevel = LOG_INFO; -int bufferStartFill = -1; - -#ifdef _WIN32 -#define DEVNULL "nul" -#else -#define DEVNULL "/dev/null" -#endif -#define RSA_LOG_LEVEL LOG_DEBUG_VV -#define SOCKET_LOG_LEVEL LOG_DEBUG_VV -#define HEADER_LOG_LEVEL LOG_DEBUG -#define AVAHI_LOG_LEVEL LOG_DEBUG - -static void handleClient(int pSock, char *pPassword, char *pHWADDR); -static void writeDataToClient(int pSock, struct shairbuffer *pResponse); -static int readDataFromClient(int pSock, struct shairbuffer *pClientBuffer); - -static int parseMessage(struct connection *pConn,unsigned char *pIpBin, unsigned int pIpBinLen, char *pHWADDR); -static void propogateCSeq(struct connection *pConn); - -static void cleanupBuffers(struct connection *pConnection); -static void cleanup(struct connection *pConnection); - -static int startAvahi(const char *pHwAddr, const char *pServerName, int pPort); - -static int getAvailChars(struct shairbuffer *pBuf); -static void addToShairBuffer(struct shairbuffer *pBuf, char *pNewBuf); -static void addNToShairBuffer(struct shairbuffer *pBuf, char *pNewBuf, int pNofNewBuf); - -static char *getTrimmedMalloc(char *pChar, int pSize, int pEndStr, int pAddNL); -static char *getTrimmed(char *pChar, int pSize, int pEndStr, int pAddNL, char *pTrimDest); - -static void slog(int pLevel, char *pFormat, ...); -static int isLogEnabledFor(int pLevel); - -static void initConnection(struct connection *pConn, struct keyring *pKeys, struct comms *pComms, int pSocket, char *pPassword); -static void closePipe(int *pPipe); -static void initBuffer(struct shairbuffer *pBuf, int pNumChars); - -static void setKeys(struct keyring *pKeys, char *pIV, char* pAESKey, char *pFmtp); -static RSA *loadKey(void); +#include +#include "common.h" +#include "rtsp.h" -static void handle_sigchld(int signo) { - int status; - waitpid(-1, &status, WNOHANG); +static void shutdown(void) { + printf("Shutting down...\n"); + rtsp_shutdown_stream(); + config.output->deinit(); + exit(0); } -int main(int argc, char **argv) -{ - // unfortunately we can't just IGN on non-SysV systems - struct sigaction sa; - sa.sa_handler = handle_sigchld; - sa.sa_flags = 0; - sigemptyset(&sa.sa_mask); - if (sigaction(SIGCHLD, &sa, NULL) < 0) { - perror("sigaction"); - return 1; - } - - char tHWID[HWID_SIZE] = {0,51,52,53,54,55}; - char tHWID_Hex[HWID_SIZE * 2 + 1]; - memset(tHWID_Hex, 0, sizeof(tHWID_Hex)); - - char tServerName[56] = "ShairPort"; - char tPassword[56] = ""; - - struct addrinfo *tAddrInfo; - int tSimLevel = 0; - int tUseKnownHWID = FALSE; - int tDaemonize = FALSE; - int tPort = PORT; - - char *arg; - while ( (arg = *++argv) ) { - if(!strcmp(arg, "-a")) - { - strncpy(tServerName, *++argv, 55); - argc--; - } - else if(!strncmp(arg, "--apname=", 9)) - { - strncpy(tServerName, arg+9, 55); - } - else if(!strcmp(arg, "-p")) - { - strncpy(tPassword, *++argv, 55); - argc--; - } - else if(!strncmp(arg, "--password=",11 )) - { - strncpy(tPassword, arg+11, 55); - } - else if(!strcmp(arg, "-o")) - { - tPort = atoi(*++argv); - argc--; - } - else if(!strncmp(arg, "--server_port=", 14)) - { - tPort = atoi(arg+14); - } - else if(!strcmp(arg, "-b")) - { - bufferStartFill = atoi(*++argv); - argc--; - } - else if(!strncmp(arg, "--buffer=", 9)) - { - bufferStartFill = atoi(arg + 9); - } - else if(!strcmp(arg, "-k")) - { - tUseKnownHWID = TRUE; - } - else if(!strcmp(arg, "-q") || !strncmp(arg, "--quiet", 7)) - { - kCurrentLogLevel = 0; - } - else if(!strcmp(arg, "-d")) - { - tDaemonize = TRUE; - kCurrentLogLevel = 0; - } - else if(!strcmp(arg, "-v")) - { - kCurrentLogLevel = LOG_DEBUG; - } - else if(!strcmp(arg, "-v2")) - { - kCurrentLogLevel = LOG_DEBUG_V; - } - else if(!strcmp(arg, "-vv") || !strcmp(arg, "-v3")) - { - kCurrentLogLevel = LOG_DEBUG_VV; - } - else if(!strcmp(arg, "-h") || !strcmp(arg, "--help")) - { - slog(LOG_INFO, "ShairPort version 0.05 C port - Airport Express emulator\n"); - slog(LOG_INFO, "Usage:\nshairport [OPTION...]\n\nOptions:\n"); - slog(LOG_INFO, " -a, --apname=AirPort Sets Airport name\n"); - slog(LOG_INFO, " -p, --password=secret Sets Password (not working)\n"); - slog(LOG_INFO, " -o, --server_port=5002 Sets Port for Avahi/dns-sd/howl\n"); - slog(LOG_INFO, " -b, --buffer=282 Sets Number of frames to buffer before beginning playback\n"); - slog(LOG_INFO, " -d Daemon mode\n"); - slog(LOG_INFO, " -q, --quiet Supresses all output.\n"); - slog(LOG_INFO, " -v,-v2,-v3,-vv Various debugging levels\n"); - slog(LOG_INFO, "\n"); - return 0; - } - } - - if ( bufferStartFill < 30 || bufferStartFill > BUFFER_FRAMES ) { - fprintf(stderr, "buffer value must be > 30 and < %d\n", BUFFER_FRAMES); - return(0); - } - - if(tDaemonize) - { - int tPid = fork(); - if(tPid < 0) - { - exit(1); // Error on fork - } - else if(tPid > 0) - { - exit(0); - } - else - { - setsid(); - int tIdx = 0; - for(tIdx = getdtablesize(); tIdx >= 0; --tIdx) - { - close(tIdx); - } - tIdx = open(DEVNULL, O_RDWR); - dup(tIdx); - dup(tIdx); - } - } - srandom ( time(NULL) ); - // Copy over empty 00's - //tPrintHWID[tIdx] = tAddr[0]; - - int tIdx = 0; - for(tIdx=0;tIdx 0) - { - if(!tUseKnownHWID) - { - int tVal = ((random() % 80) + 33); - tHWID[tIdx] = tVal; - } - //tPrintHWID[tIdx] = tAddr[tIdx]; - } - sprintf(tHWID_Hex+(tIdx*2), "%02X",tHWID[tIdx]); - } - //tPrintHWID[HWID_SIZE] = '\0'; - - slog(LOG_INFO, "LogLevel: %d\n", kCurrentLogLevel); - slog(LOG_INFO, "AirName: %s\n", tServerName); - slog(LOG_INFO, "HWID: %.*s\n", HWID_SIZE, tHWID+1); - slog(LOG_INFO, "HWID_Hex(%d): %s\n", strlen(tHWID_Hex), tHWID_Hex); - - if(tSimLevel >= 1) - { - #ifdef SIM_INCL - sim(tSimLevel, tTestValue, tHWID); - #endif - return(1); - } - else - { - startAvahi(tHWID_Hex, tServerName, tPort); - slog(LOG_DEBUG_V, "Starting connection server: specified server port: %d\n", tPort); - int tServerSock = setupListenServer(&tAddrInfo, tPort); - if(tServerSock < 0) - { - freeaddrinfo(tAddrInfo); - slog(LOG_INFO, "Error setting up server socket on port %d, try specifying a different port\n", tPort); - exit(1); - } - - int tClientSock = 0; - while(1) - { - slog(LOG_DEBUG_V, "Waiting for clients to connect\n"); - tClientSock = acceptClient(tServerSock, tAddrInfo); - if(tClientSock > 0) - { - int tPid = fork(); - if(tPid == 0) - { - freeaddrinfo(tAddrInfo); - tAddrInfo = NULL; - slog(LOG_DEBUG, "...Accepted Client Connection..\n"); - close(tServerSock); - handleClient(tClientSock, tPassword, tHWID); - //close(tClientSock); - return 0; - } - else - { - slog(LOG_DEBUG_VV, "Child now busy handling new client\n"); - close(tClientSock); - } - } - else - { - // failed to init server socket....try waiting a moment... - sleep(2); - } - } - } - - slog(LOG_DEBUG_VV, "Finished, and waiting to clean everything up\n"); - sleep(1); - if(tAddrInfo != NULL) - { - freeaddrinfo(tAddrInfo); - } - return 0; +static void sig_ignore(int foo, siginfo_t *bar, void *baz) { +} +static void sig_shutdown(int foo, siginfo_t *bar, void *baz) { + shutdown(); } -static int findEnd(char *tReadBuf) -{ - // find \n\n, \r\n\r\n, or \r\r is found - int tIdx = 0; - int tLen = strlen(tReadBuf); - for(tIdx = 0; tIdx < tLen; tIdx++) - { - if(tReadBuf[tIdx] == '\r') - { - if(tIdx + 1 < tLen) - { - if(tReadBuf[tIdx+1] == '\r') - { - return (tIdx+1); - } - else if(tIdx+3 < tLen) - { - if(tReadBuf[tIdx+1] == '\n' && - tReadBuf[tIdx+2] == '\r' && - tReadBuf[tIdx+3] == '\n') - { - return (tIdx+3); - } - } - } - } - else if(tReadBuf[tIdx] == '\n') - { - if(tIdx + 1 < tLen && tReadBuf[tIdx+1] == '\n') - { - return (tIdx + 1); - } - } - } - // Found nothing - return -1; -} -static void handleClient(int pSock, char *pPassword, char *pHWADDR) -{ - slog(LOG_DEBUG_VV, "In Handle Client\n"); - fflush(stdout); +int main(int argc, char **argv) { + memset(&config, 0, sizeof(config)); +// config.password = "hello"; + memcpy(config.hw_addr, "\0\x11\x22\x33\x44\x55", 6); + config.buffer_start_fill = 220; - socklen_t len; - struct sockaddr_storage addr; - #ifdef AF_INET6 - unsigned char ipbin[INET6_ADDRSTRLEN]; - #else - unsigned char ipbin[INET_ADDRSTRLEN]; - #endif - unsigned int ipbinlen; - int port; - char ipstr[64]; + config.output = &audio_dummy; + //config.output = &audio_ao; - len = sizeof addr; - getsockname(pSock, (struct sockaddr*)&addr, &len); + // mask off all signals before creating threads. + // this way we control which thread gets which signals. + sigset_t set; + sigfillset(&set); + sigdelset(&set, SIGINT); + sigdelset(&set, SIGTERM); + sigdelset(&set, SIGSTOP); + pthread_sigmask(SIG_BLOCK, &set, NULL); - // deal with both IPv4 and IPv6: - if (addr.ss_family == AF_INET) { - slog(LOG_DEBUG_V, "Constructing ipv4 address\n"); - struct sockaddr_in *s = (struct sockaddr_in *)&addr; - port = ntohs(s->sin_port); - inet_ntop(AF_INET, &s->sin_addr, ipstr, sizeof ipstr); - memcpy(ipbin, &s->sin_addr, 4); - ipbinlen = 4; - } else { // AF_INET6 - slog(LOG_DEBUG_V, "Constructing ipv6 address\n"); - struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; - port = ntohs(s->sin6_port); - inet_ntop(AF_INET6, &s->sin6_addr, ipstr, sizeof ipstr); - - union { - struct sockaddr_in6 s; - unsigned char bin[sizeof(struct sockaddr_in6)]; - } addr; - memcpy(&addr.s, &s->sin6_addr, sizeof(struct sockaddr_in6)); - - if(memcmp(&addr.bin[0], "\x00\x00\x00\x00" "\x00\x00\x00\x00" "\x00\x00\xff\xff", 12) == 0) - { - // its ipv4... - memcpy(ipbin, &addr.bin[12], 4); - ipbinlen = 4; - } - else - { - memcpy(ipbin, &s->sin6_addr, 16); - ipbinlen = 16; - } - } - - slog(LOG_DEBUG_V, "Peer IP address: %s\n", ipstr); - slog(LOG_DEBUG_V, "Peer port : %d\n", port); - - int tMoreDataNeeded = 1; - struct keyring tKeys; - struct comms tComms; - struct connection tConn; - initConnection(&tConn, &tKeys, &tComms, pSock, pPassword); - - while(1) - { - tMoreDataNeeded = 1; - - initBuffer(&tConn.recv, 80); // Just a random, small size to seed the buffer with. - initBuffer(&tConn.resp, 80); + // setting this to SIG_IGN would prevent signalling any threads. + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = &sig_ignore; + sigaction(SIGUSR1, &sa, NULL); - int tError = FALSE; - while(1 == tMoreDataNeeded) - { - tError = readDataFromClient(pSock, &(tConn.recv)); - if(!tError && strlen(tConn.recv.data) > 0) - { - slog(LOG_DEBUG_VV, "Finished Reading some data from client\n"); - // parse client request - tMoreDataNeeded = parseMessage(&tConn, ipbin, ipbinlen, pHWADDR); - if(1 == tMoreDataNeeded) - { - slog(LOG_DEBUG_VV, "\n\nNeed to read more data\n"); - } - else if(-1 == tMoreDataNeeded) // Forked process down below ended. - { - slog(LOG_DEBUG_V, "Forked Process ended...cleaning up\n"); - cleanup(&tConn); - // pSock was already closed - return; - } - // if more data needed, - } - else - { - slog(LOG_DEBUG, "Error reading from socket, closing client\n"); - // Error reading data....quit. - cleanup(&tConn); - return; - } - } - slog(LOG_DEBUG_VV, "Writing: %d chars to socket\n", tConn.resp.current); - //tConn->resp.data[tConn->resp.current-1] = '\0'; - writeDataToClient(pSock, &(tConn.resp)); - // Finished reading one message... - cleanupBuffers(&tConn); - } - cleanup(&tConn); - fflush(stdout); -} + sa.sa_sigaction = &sig_shutdown; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); -static void writeDataToClient(int pSock, struct shairbuffer *pResponse) -{ - slog(LOG_DEBUG_VV, "\n----Beg Send Response Header----\n%.*s\n", pResponse->current, pResponse->data); - send(pSock, pResponse->data, pResponse->current,0); - slog(LOG_DEBUG_VV, "----Send Response Header----\n"); -} + config.output->init(0, 0); -static int readDataFromClient(int pSock, struct shairbuffer *pClientBuffer) -{ - char tReadBuf[MAX_SIZE]; - tReadBuf[0] = '\0'; + rtsp_listen_loop(); - int tRetval = 1; - int tEnd = -1; - while(tRetval > 0 && tEnd < 0) - { - // Read from socket until \n\n, \r\n\r\n, or \r\r is found - slog(LOG_DEBUG_V, "Waiting To Read...\n"); - fflush(stdout); - tRetval = read(pSock, tReadBuf, MAX_SIZE); - // if new buffer contains the end of request string, only copy partial buffer? - tEnd = findEnd(tReadBuf); - if(tEnd >= 0) - { - if(pClientBuffer->marker == 0) - { - pClientBuffer->marker = tEnd+1; // Marks start of content - } - slog(SOCKET_LOG_LEVEL, "Found end of http request at: %d\n", tEnd); - fflush(stdout); - } - else - { - tEnd = MAX_SIZE; - slog(SOCKET_LOG_LEVEL, "Read %d of data so far\n%s\n", tRetval, tReadBuf); - fflush(stdout); - } - if(tRetval > 0) - { - // Copy read data into tReceive; - slog(SOCKET_LOG_LEVEL, "Read %d data, using %d of it\n", tRetval, tEnd); - addNToShairBuffer(pClientBuffer, tReadBuf, tRetval); - slog(LOG_DEBUG_VV, "Finished copying data\n"); - } - else - { - slog(LOG_DEBUG, "Error reading data from socket, got: %d bytes", tRetval); - return tRetval; - } - } - if(tEnd + 1 != tRetval) - { - slog(SOCKET_LOG_LEVEL, "Read more data after end of http request. %d instead of %d\n", tRetval, tEnd+1); - } - slog(SOCKET_LOG_LEVEL, "Finished Reading Data:\n%s\nEndOfData\n", pClientBuffer->data); - fflush(stdout); - return 0; -} - -static char *getFromBuffer(char *pBufferPtr, const char *pField, int pLenAfterField, int *pReturnSize, char *pDelims) -{ - slog(LOG_DEBUG_V, "GettingFromBuffer: %s\n", pField); - char* tFound = strstr(pBufferPtr, pField); - int tSize = 0; - if(tFound != NULL) - { - tFound += (strlen(pField) + pLenAfterField); - int tIdx = 0; - char tDelim = pDelims[tIdx]; - char *tShortest = NULL; - char *tEnd = NULL; - while(tDelim != '\0') - { - tDelim = pDelims[tIdx++]; // Ensures that \0 is also searched. - tEnd = strchr(tFound, tDelim); - if(tEnd != NULL && (NULL == tShortest || tEnd < tShortest)) - { - tShortest = tEnd; - } - } - - tSize = (int) (tShortest - tFound); - slog(LOG_DEBUG_VV, "Found %.*s length: %d\n", tSize, tFound, tSize); - if(pReturnSize != NULL) - { - *pReturnSize = tSize; - } - } - else - { - slog(LOG_DEBUG_V, "Not Found\n"); - } - return tFound; -} - -static char *getFromHeader(char *pHeaderPtr, const char *pField, int *pReturnSize) -{ - return getFromBuffer(pHeaderPtr, pField, 2, pReturnSize, "\r\n"); -} - -static char *getFromContent(char *pContentPtr, const char* pField, int *pReturnSize) -{ - return getFromBuffer(pContentPtr, pField, 1, pReturnSize, "\r\n"); -} - -static char *getFromSetup(char *pContentPtr, const char* pField, int *pReturnSize) -{ - return getFromBuffer(pContentPtr, pField, 1, pReturnSize, ";\r\n"); -} - -// Handles compiling the Apple-Challenge, HWID, and Server IP Address -// Into the response the airplay client is expecting. -static int buildAppleResponse(struct connection *pConn, unsigned char *pIpBin, - unsigned int pIpBinLen, char *pHWID) -{ - // Find Apple-Challenge - char *tResponse = NULL; - - int tFoundSize = 0; - char* tFound = getFromHeader(pConn->recv.data, "Apple-Challenge", &tFoundSize); - if(tFound != NULL) - { - char tTrim[tFoundSize + 2]; - getTrimmed(tFound, tFoundSize, TRUE, TRUE, tTrim); - slog(LOG_DEBUG_VV, "HeaderChallenge: [%s] len: %d sizeFound: %d\n", tTrim, strlen(tTrim), tFoundSize); - int tChallengeDecodeSize = 16; - char *tChallenge = decode_base64((unsigned char *)tTrim, tFoundSize, &tChallengeDecodeSize); - slog(LOG_DEBUG_VV, "Challenge Decode size: %d expected 16\n", tChallengeDecodeSize); - - int tCurSize = 0; - unsigned char tChalResp[38]; - - memcpy(tChalResp, tChallenge, tChallengeDecodeSize); - tCurSize += tChallengeDecodeSize; - - memcpy(tChalResp+tCurSize, pIpBin, pIpBinLen); - tCurSize += pIpBinLen; - - memcpy(tChalResp+tCurSize, pHWID, HWID_SIZE); - tCurSize += HWID_SIZE; - - int tPad = 32 - tCurSize; - if (tPad > 0) - { - memset(tChalResp+tCurSize, 0, tPad); - tCurSize += tPad; - } - - char *tTmp = encode_base64((unsigned char *)tChalResp, tCurSize); - slog(LOG_DEBUG_VV, "Full sig: %s\n", tTmp); - free(tTmp); - - // RSA Encrypt - RSA *rsa = loadKey(); // Free RSA - int tSize = RSA_size(rsa); - unsigned char tTo[tSize]; - RSA_private_encrypt(tCurSize, (unsigned char *)tChalResp, tTo, rsa, RSA_PKCS1_PADDING); - - // Wrap RSA Encrypted binary in Base64 encoding - tResponse = encode_base64(tTo, tSize); - int tLen = strlen(tResponse); - while(tLen > 1 && tResponse[tLen-1] == '=') - { - tResponse[tLen-1] = '\0'; - } - free(tChallenge); - RSA_free(rsa); - } - - if(tResponse != NULL) - { - // Append to current response - addToShairBuffer(&(pConn->resp), "Apple-Response: "); - addToShairBuffer(&(pConn->resp), tResponse); - addToShairBuffer(&(pConn->resp), "\r\n"); - free(tResponse); - return TRUE; - } - return FALSE; -} - -//parseMessage(tConn->recv.data, tConn->recv.mark, &tConn->resp, ipstr, pHWADDR, tConn->keys); -static int parseMessage(struct connection *pConn, unsigned char *pIpBin, unsigned int pIpBinLen, char *pHWID) -{ - int tReturn = 0; // 0 = good, 1 = Needs More Data, -1 = close client socket. - if(pConn->resp.data == NULL) - { - initBuffer(&(pConn->resp), MAX_SIZE); - } - - char *tContent = getFromHeader(pConn->recv.data, "Content-Length", NULL); - if(tContent != NULL) - { - int tContentSize = atoi(tContent); - if(pConn->recv.marker == 0 || strlen(pConn->recv.data+pConn->recv.marker) != tContentSize) - { - if(isLogEnabledFor(HEADER_LOG_LEVEL)) - { - slog(HEADER_LOG_LEVEL, "Content-Length: %s value -> %d\n", tContent, tContentSize); - if(pConn->recv.marker != 0) - { - slog(HEADER_LOG_LEVEL, "ContentPtr has %d, but needs %d\n", - strlen(pConn->recv.data+pConn->recv.marker), tContentSize); - } - } - // check if value in tContent > 2nd read from client. - return 1; // means more content-length needed - } - } - else - { - slog(LOG_DEBUG_VV, "No content, header only\n"); - } - - // "Creates" a new Response Header for our response message - addToShairBuffer(&(pConn->resp), "RTSP/1.0 200 OK\r\n"); - - if(isLogEnabledFor(LOG_INFO)) - { - int tLen = strchr(pConn->recv.data, ' ') - pConn->recv.data; - if(tLen < 0 || tLen > 20) - { - tLen = 20; - } - slog(LOG_INFO, "********** RECV %.*s **********\n", tLen, pConn->recv.data); - } - - if(pConn->password != NULL) - { - - } - - if(buildAppleResponse(pConn, pIpBin, pIpBinLen, pHWID)) // need to free sig - { - slog(LOG_DEBUG_V, "Added AppleResponse to Apple-Challenge request\n"); - } - - // Find option, then based on option, do different actions. - if(strncmp(pConn->recv.data, "OPTIONS", 7) == 0) - { - propogateCSeq(pConn); - addToShairBuffer(&(pConn->resp), - "Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER\r\n"); - } - else if(!strncmp(pConn->recv.data, "ANNOUNCE", 8)) - { - char *tContent = pConn->recv.data + pConn->recv.marker; - int tSize = 0; - char *tHeaderVal = getFromContent(tContent, "a=aesiv", &tSize); // Not allocated memory, just pointing - if(tSize > 0) - { - int tKeySize = 0; - char tEncodedAesIV[tSize + 2]; - getTrimmed(tHeaderVal, tSize, TRUE, TRUE, tEncodedAesIV); - slog(LOG_DEBUG_VV, "AESIV: [%.*s] Size: %d Strlen: %d\n", tSize, tEncodedAesIV, tSize, strlen(tEncodedAesIV)); - char *tDecodedIV = decode_base64((unsigned char*) tEncodedAesIV, tSize, &tSize); - - // grab the key, copy it out of the receive buffer - tHeaderVal = getFromContent(tContent, "a=rsaaeskey", &tKeySize); - char tEncodedAesKey[tKeySize + 2]; // +1 for nl, +1 for \0 - getTrimmed(tHeaderVal, tKeySize, TRUE, TRUE, tEncodedAesKey); - slog(LOG_DEBUG_VV, "AES KEY: [%s] Size: %d Strlen: %d\n", tEncodedAesKey, tKeySize, strlen(tEncodedAesKey)); - // remove base64 coding from key - char *tDecodedAesKey = decode_base64((unsigned char*) tEncodedAesKey, - tKeySize, &tKeySize); // Need to free DecodedAesKey - - // Grab the formats - int tFmtpSize = 0; - char *tFmtp = getFromContent(tContent, "a=fmtp", &tFmtpSize); // Don't need to free - tFmtp = getTrimmedMalloc(tFmtp, tFmtpSize, TRUE, FALSE); // will need to free - slog(LOG_DEBUG_VV, "Format: %s\n", tFmtp); - - RSA *rsa = loadKey(); - // Decrypt the binary aes key - char *tDecryptedKey = malloc(RSA_size(rsa) * sizeof(char)); // Need to Free Decrypted key - //char tDecryptedKey[RSA_size(rsa)]; - if(RSA_private_decrypt(tKeySize, (unsigned char *)tDecodedAesKey, - (unsigned char*) tDecryptedKey, rsa, RSA_PKCS1_OAEP_PADDING) >= 0) - { - slog(LOG_DEBUG, "Decrypted AES key from RSA Successfully\n"); - } - else - { - slog(LOG_INFO, "Error Decrypting AES key from RSA\n"); - } - free(tDecodedAesKey); - RSA_free(rsa); - - setKeys(pConn->keys, tDecodedIV, tDecryptedKey, tFmtp); - - propogateCSeq(pConn); - } - } - else if(!strncmp(pConn->recv.data, "SETUP", 5)) - { - // Setup pipes - struct comms *tComms = pConn->hairtunes; - if (! (pipe(tComms->in) == 0 && pipe(tComms->out) == 0)) - { - slog(LOG_INFO, "Error setting up hairtunes communications...some things probably wont work very well.\n"); - } - - // Setup fork - char tPort[8] = "6000"; // get this from dup()'d stdout of child pid - - int tPid = fork(); - if(tPid == 0) - { - int tDataport=0; - char tCPortStr[8] = "59010"; - char tTPortStr[8] = "59012"; - int tSize = 0; - - char *tFound =getFromSetup(pConn->recv.data, "control_port", &tSize); - getTrimmed(tFound, tSize, 1, 0, tCPortStr); - tFound = getFromSetup(pConn->recv.data, "timing_port", &tSize); - getTrimmed(tFound, tSize, 1, 0, tTPortStr); - - slog(LOG_DEBUG_VV, "converting %s and %s from str->int\n", tCPortStr, tTPortStr); - int tControlport = atoi(tCPortStr); - int tTimingport = atoi(tTPortStr); - - slog(LOG_DEBUG_V, "Got %d for CPort and %d for TPort\n", tControlport, tTimingport); - char *tRtp = NULL; - char *tPipe = NULL; - char *tAoDriver = NULL; - char *tAoDeviceName = NULL; - char *tAoDeviceId = NULL; - - // ************************************************* - // ** Setting up Pipes, AKA no more debug/output ** - // ************************************************* - dup2(tComms->in[0],0); // Input to child - closePipe(&(tComms->in[0])); - closePipe(&(tComms->in[1])); - - dup2(tComms->out[1], 1); // Output from child - closePipe(&(tComms->out[1])); - closePipe(&(tComms->out[0])); - - struct keyring *tKeys = pConn->keys; - pConn->keys = NULL; - pConn->hairtunes = NULL; - - // Free up any recv buffers, etc.. - if(pConn->clientSocket != -1) - { - close(pConn->clientSocket); - pConn->clientSocket = -1; - } - cleanupBuffers(pConn); - hairtunes_init(tKeys->aeskey, tKeys->aesiv, tKeys->fmt, tControlport, tTimingport, - tDataport, tRtp, tPipe, tAoDriver, tAoDeviceName, tAoDeviceId, - bufferStartFill); - - // Quit when finished. - slog(LOG_DEBUG, "Returned from hairtunes init....returning -1, should close out this whole side of the fork\n"); - return -1; - } - else if(tPid >0) - { - // Ensure Connection has access to the pipe. - closePipe(&(tComms->in[0])); - closePipe(&(tComms->out[1])); - - char tFromHairtunes[80]; - int tRead = read(tComms->out[0], tFromHairtunes, 80); - if(tRead <= 0) - { - slog(LOG_INFO, "Error reading port from hairtunes function, assuming default port: %d\n", tPort); - } - else - { - int tSize = 0; - char *tPortStr = getFromHeader(tFromHairtunes, "port", &tSize); - if(tPortStr != NULL) - { - getTrimmed(tPortStr, tSize, TRUE, FALSE, tPort); - } - else - { - slog(LOG_INFO, "Read %d bytes, Error translating %s into a port\n", tRead, tFromHairtunes); - } - } - // READ Ports from here?close(pConn->hairtunes_pipes[0]); - propogateCSeq(pConn); - int tSize = 0; - char *tTransport = getFromHeader(pConn->recv.data, "Transport", &tSize); - addToShairBuffer(&(pConn->resp), "Transport: "); - addNToShairBuffer(&(pConn->resp), tTransport, tSize); - // Append server port: - addToShairBuffer(&(pConn->resp), ";server_port="); - addToShairBuffer(&(pConn->resp), tPort); - addToShairBuffer(&(pConn->resp), "\r\nSession: DEADBEEF\r\n"); - } - else - { - slog(LOG_INFO, "Error forking process....dere' be errors round here.\n"); - return -1; - } - } - else if(!strncmp(pConn->recv.data, "TEARDOWN", 8)) - { - // Be smart? Do more finish up stuff... - addToShairBuffer(&(pConn->resp), "Connection: close\r\n"); - propogateCSeq(pConn); - close(pConn->hairtunes->in[1]); - slog(LOG_DEBUG, "Tearing down connection, closing pipes\n"); - //close(pConn->hairtunes->out[0]); - tReturn = -1; // Close client socket, but sends an ACK/OK packet first - } - else if(!strncmp(pConn->recv.data, "FLUSH", 5)) - { - write(pConn->hairtunes->in[1], "flush\n", 6); - propogateCSeq(pConn); - } - else if(!strncmp(pConn->recv.data, "SET_PARAMETER", 13)) - { - propogateCSeq(pConn); - int tSize = 0; - char *tVol = getFromHeader(pConn->recv.data, "volume", &tSize); - slog(LOG_DEBUG_VV, "About to write [vol: %.*s] data to hairtunes\n", tSize, tVol); - - write(pConn->hairtunes->in[1], "vol: ", 5); - write(pConn->hairtunes->in[1], tVol, tSize); - write(pConn->hairtunes->in[1], "\n", 1); - slog(LOG_DEBUG_VV, "Finished writing data write data to hairtunes\n"); - } - else - { - slog(LOG_DEBUG, "\n\nUn-Handled recv: %s\n", pConn->recv.data); - propogateCSeq(pConn); - } - addToShairBuffer(&(pConn->resp), "\r\n"); - return tReturn; -} - -// Copies CSeq value from request, and adds standard header values in. -static void propogateCSeq(struct connection *pConn) //char *pRecvBuffer, struct shairbuffer *pConn->recp.data) -{ - int tSize=0; - char *tRecPtr = getFromHeader(pConn->recv.data, "CSeq", &tSize); - addToShairBuffer(&(pConn->resp), "Audio-Jack-Status: connected; type=analog\r\n"); - addToShairBuffer(&(pConn->resp), "CSeq: "); - addNToShairBuffer(&(pConn->resp), tRecPtr, tSize); - addToShairBuffer(&(pConn->resp), "\r\n"); -} - -void cleanupBuffers(struct connection *pConn) -{ - if(pConn->recv.data != NULL) - { - free(pConn->recv.data); - pConn->recv.data = NULL; - } - if(pConn->resp.data != NULL) - { - free(pConn->resp.data); - pConn->resp.data = NULL; - } -} - -static void cleanup(struct connection *pConn) -{ - cleanupBuffers(pConn); - if(pConn->hairtunes != NULL) - { - - closePipe(&(pConn->hairtunes->in[0])); - closePipe(&(pConn->hairtunes->in[1])); - closePipe(&(pConn->hairtunes->out[0])); - closePipe(&(pConn->hairtunes->out[1])); - } - if(pConn->keys != NULL) - { - if(pConn->keys->aesiv != NULL) - { - free(pConn->keys->aesiv); - } - if(pConn->keys->aeskey != NULL) - { - free(pConn->keys->aeskey); - } - if(pConn->keys->fmt != NULL) - { - free(pConn->keys->fmt); - } - pConn->keys = NULL; - } - if(pConn->clientSocket != -1) - { - close(pConn->clientSocket); - pConn->clientSocket = -1; - } -} - -static int startAvahi(const char *pHWStr, const char *pServerName, int pPort) -{ - int tMaxServerName = 25; // Something reasonable? iPad showed 21, iphone 25 - int tPid = fork(); - if(tPid == 0) - { - char tName[100 + HWID_SIZE + 3]; - if(strlen(pServerName) > tMaxServerName) - { - slog(LOG_INFO,"Hey dog, we see you like long server names, " - "so we put a strncat in our command so we don't buffer overflow, while you listen to your flow.\n" - "We just used the first %d characters. Pick something shorter if you want\n", tMaxServerName); - } - - tName[0] = '\0'; - char tPort[SERVLEN]; - sprintf(tPort, "%d", pPort); - strcat(tName, pHWStr); - strcat(tName, "@"); - strncat(tName, pServerName, tMaxServerName); - slog(AVAHI_LOG_LEVEL, "Avahi/DNS-SD Name: %s\n", tName); - - execlp("avahi-publish-service", "avahi-publish-service", tName, - "_raop._tcp", tPort, "tp=UDP","sm=false","sv=false","ek=1","et=0,1", - "cn=0,1","ch=2","ss=16","sr=44100","pw=false","vn=3","txtvers=1", NULL); - execlp("dns-sd", "dns-sd", "-R", tName, - "_raop._tcp", ".", tPort, "tp=UDP","sm=false","sv=false","ek=1","et=0,1", - "cn=0,1","ch=2","ss=16","sr=44100","pw=false","vn=3","txtvers=1", NULL); - execlp("mDNSPublish", "mDNSPublish", tName, - "_raop._tcp", tPort, "tp=UDP","sm=false","sv=false","ek=1","et=0,1", - "cn=0,1","ch=2","ss=16","sr=44100","pw=false","vn=3","txtvers=1", NULL); - if(errno == -1) { - perror("error"); - } - - slog(LOG_INFO, "Bad error... couldn't find or failed to run: avahi-publish-service OR dns-sd OR mDNSPublish\n"); - exit(1); - } - else - { - slog(LOG_DEBUG_VV, "Avahi/DNS-SD started on PID: %d\n", tPid); - } - return tPid; -} - -static int getAvailChars(struct shairbuffer *pBuf) -{ - return (pBuf->maxsize / sizeof(char)) - pBuf->current; -} - -static void addToShairBuffer(struct shairbuffer *pBuf, char *pNewBuf) -{ - addNToShairBuffer(pBuf, pNewBuf, strlen(pNewBuf)); -} - -static void addNToShairBuffer(struct shairbuffer *pBuf, char *pNewBuf, int pNofNewBuf) -{ - int tAvailChars = getAvailChars(pBuf); - if(pNofNewBuf > tAvailChars) - { - int tNewSize = pBuf->maxsize * 2 + MAX_SIZE + sizeof(char); - char *tTmpBuf = malloc(tNewSize); - - tTmpBuf[0] = '\0'; - memset(tTmpBuf, 0, tNewSize/sizeof(char)); - memcpy(tTmpBuf, pBuf->data, pBuf->current); - free(pBuf->data); - - pBuf->maxsize = tNewSize; - pBuf->data = tTmpBuf; - } - memcpy(pBuf->data + pBuf->current, pNewBuf, pNofNewBuf); - pBuf->current += pNofNewBuf; - if(getAvailChars(pBuf) > 1) - { - pBuf->data[pBuf->current] = '\0'; - } -} - -static char *getTrimmedMalloc(char *pChar, int pSize, int pEndStr, int pAddNL) -{ - int tAdditionalSize = 0; - if(pEndStr) - tAdditionalSize++; - if(pAddNL) - tAdditionalSize++; - char *tTrimDest = malloc(sizeof(char) * (pSize + tAdditionalSize)); - return getTrimmed(pChar, pSize, pEndStr, pAddNL, tTrimDest); -} - -// Must free returned ptr -static char *getTrimmed(char *pChar, int pSize, int pEndStr, int pAddNL, char *pTrimDest) -{ - int tSize = pSize; - if(pEndStr) - { - tSize++; - } - if(pAddNL) - { - tSize++; - } - - memset(pTrimDest, 0, tSize); - memcpy(pTrimDest, pChar, pSize); - if(pAddNL) - { - pTrimDest[pSize] = '\n'; - } - if(pEndStr) - { - pTrimDest[tSize-1] = '\0'; - } - return pTrimDest; -} - -static void slog(int pLevel, char *pFormat, ...) -{ - #ifdef SHAIRPORT_LOG - if(isLogEnabledFor(pLevel)) - { - va_list argp; - va_start(argp, pFormat); - vprintf(pFormat, argp); - va_end(argp); - } - #endif -} - -static int isLogEnabledFor(int pLevel) -{ - if(pLevel <= kCurrentLogLevel) - { - return TRUE; - } - return FALSE; -} - -static void initConnection(struct connection *pConn, struct keyring *pKeys, - struct comms *pComms, int pSocket, char *pPassword) -{ - pConn->hairtunes = pComms; - if(pKeys != NULL) - { - pConn->keys = pKeys; - pConn->keys->aesiv = NULL; - pConn->keys->aeskey = NULL; - pConn->keys->fmt = NULL; - } - pConn->recv.data = NULL; // Pre-init buffer expected to be NULL - pConn->resp.data = NULL; // Pre-init buffer expected to be NULL - pConn->clientSocket = pSocket; - if(strlen(pPassword) >0) - { - pConn->password = pPassword; - } - else - { - pConn->password = NULL; - } -} - -static void closePipe(int *pPipe) -{ - if(*pPipe != -1) - { - close(*pPipe); - *pPipe = -1; - } -} - -static void initBuffer(struct shairbuffer *pBuf, int pNumChars) -{ - if(pBuf->data != NULL) - { - slog(LOG_DEBUG_VV, "Hrm, buffer wasn't cleaned up....trying to free\n"); - free(pBuf->data); - slog(LOG_DEBUG_VV, "Free didn't seem to seg fault....huzzah\n"); - } - pBuf->current = 0; - pBuf->marker = 0; - pBuf->maxsize = sizeof(char) * pNumChars; - pBuf->data = malloc(pBuf->maxsize); - memset(pBuf->data, 0, pBuf->maxsize); -} - -static void setKeys(struct keyring *pKeys, char *pIV, char* pAESKey, char *pFmtp) -{ - if(pKeys->aesiv != NULL) - { - free(pKeys->aesiv); - } - if(pKeys->aeskey != NULL) - { - free(pKeys->aeskey); - } - if(pKeys->fmt != NULL) - { - free(pKeys->fmt); - } - pKeys->aesiv = pIV; - pKeys->aeskey = pAESKey; - pKeys->fmt = pFmtp; -} - -#define AIRPORT_PRIVATE_KEY \ -"-----BEGIN RSA PRIVATE KEY-----\n" \ -"MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt\n" \ -"wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U\n" \ -"wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf\n" \ -"/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/\n" \ -"UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW\n" \ -"BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa\n" \ -"LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5\n" \ -"NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm\n" \ -"lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz\n" \ -"aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu\n" \ -"a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM\n" \ -"oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z\n" \ -"oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+\n" \ -"k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL\n" \ -"AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA\n" \ -"cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf\n" \ -"54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov\n" \ -"17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc\n" \ -"1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI\n" \ -"LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ\n" \ -"2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY=\n" \ -"-----END RSA PRIVATE KEY-----" - -static RSA *loadKey(void) -{ - BIO *tBio = BIO_new_mem_buf(AIRPORT_PRIVATE_KEY, -1); - RSA *rsa = PEM_read_bio_RSAPrivateKey(tBio, NULL, NULL, NULL); //NULL, NULL, NULL); - BIO_free(tBio); - slog(RSA_LOG_LEVEL, "RSA Key: %d\n", RSA_check_key(rsa)); - return rsa; + // should not. + shutdown(); + return 1; } diff --git a/shairport.h b/shairport.h deleted file mode 100644 index 901a1722..00000000 --- a/shairport.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef __SHAIRPORT_H__ -#define __SHAIRPORT_H__ - -#include -#include -#include -#include -#include -#include -#include "socketlib.h" -#include -#include -#include -#include - - -#define HWID_SIZE 6 -#define SHAIRPORT_LOG 1 -#define LOG_INFO 1 -#define LOG_DEBUG 5 -#define LOG_DEBUG_V 6 -#define LOG_DEBUG_VV 7 - -struct shairbuffer -{ - char *data; - int current; - int maxsize; - int marker; -}; - -struct keyring -{ - char *aeskey; - char *aesiv; - char *fmt; -}; - -struct comms -{ - int in[2]; - int out[2]; -}; - -struct connection -{ - struct shairbuffer recv; - struct shairbuffer resp; - struct keyring *keys; // Does not point to malloc'd memory. - struct comms *hairtunes; - int clientSocket; - char *password; -}; - -void sim(int pLevel, char *pValue1, char *pValue2); - -#endif diff --git a/shairport.init.sample b/shairport.init.sample deleted file mode 100755 index e7495b9d..00000000 --- a/shairport.init.sample +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -# -# This starts and stops shairport -# -### BEGIN INIT INFO -# Provides: shairport -# Required-Start: $network -# Required-Stop: -# Short-Description: shairport - Airtunes emulator! -# Description: Airtunes emulator! -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -### END INIT INFO - - -# Source function library. -. /lib/lsb/init-functions - -NAME=ShairPort -DAEMON="/usr/local/bin/shairport.pl" -PIDFILE=/var/run/$NAME.pid -DAEMON_ARGS="-w $PIDFILE -a $NAME" - -[ -x $binary ] || exit 0 - -RETVAL=0 - -start() { - echo -n "Starting shairport: " - start-stop-daemon --start --quiet --pidfile "$PIDFILE" \ - --exec "$DAEMON" -b --oknodo -- $DAEMON_ARGS - log_end_msg $? -} - -stop() { - echo -n "Shutting down shairport: " - start-stop-daemon --stop --quiet --pidfile "$PIDFILE" \ - --retry 1 --oknodo - log_end_msg $? -} - -restart() { - stop - sleep 1 - start -} - -case "$1" in - start) - start - ;; - stop) - stop - ;; - status) - status shairport - ;; - restart) - restart - ;; - *) - echo "Usage: $0 {start|stop|status|restart}" - ;; -esac - -exit 0 - diff --git a/shairport.pl b/shairport.pl deleted file mode 100755 index 2da0e8e2..00000000 --- a/shairport.pl +++ /dev/null @@ -1,763 +0,0 @@ -#!/usr/bin/env perl - -# ShairPort - Airtunes compatible server -# Copyright (c) 2011 James Laird -# All rights reserved. -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -use strict; -use warnings; - -use 5.10.0; -# For given() { when() { } ... } -use feature ":5.10"; - -use Getopt::Long; -use FindBin; -use File::Basename; - -use IO::Select; -use IO::Socket; -use MIME::Base64; -use HTTP::Request; -use HTTP::Response; -use URI::Escape; -use IPC::Open2; -use Crypt::OpenSSL::RSA; -use Digest::MD5 qw(md5 md5_hex); -use POSIX qw(:sys_wait_h setsid); -eval "use IO::Socket::INET6;"; -use Net::SDP; - -my $shairportversion = "0.05"; - -my $apname = "ShairPort $$ on " . `hostname`; -my $port = 5002; -# password - required to connect -# for no password, set: -my $password = ''; -# output to a pipe? -my $pipepath; -# detach -my $daemon; -# ao options -my $libao_driver; -my $libao_devicename; -my $libao_deviceid; -# suppose hairtunes is under same directory -my $hairtunes_cli = $FindBin::Bin . '/hairtunes'; -# Integrate with Squeezebox Server -my $squeeze; -# SBS CLI port -my $cliport; -# SB target -my $mac; -# SB volume -my $volume; -# custom play and stop program -my $play_prog; -my $stop_prog; -# output debugging information -my $verbose; -# where to write PID -my $writepid; -# show help -my $help; - - -my $ipv4_only; - -unless (-x $hairtunes_cli) { - say "Can't find the 'hairtunes' decoder binary, you need to build this before using ShairPort."; - say "Read the INSTALL instructions!"; - exit(1); -} - -GetOptions("a|apname=s" => \$apname, - "p|password=s" => \$password, - "o|server_port=s" => \$port, - "i|pipe=s" => \$pipepath, - "d" => \$daemon, - "ao_driver=s" => \$libao_driver, - "ao_devicename=s" => \$libao_devicename, - "ao_deviceid=s" => \$libao_deviceid, - "v|verbose" => \$verbose, - "4" => \$ipv4_only, - "w|writepid=s" => \$writepid, - "s|squeezebox" => \$squeeze, - "c|cliport=s" => \$cliport, - "m|mac=s" => \$mac, - "play_prog=s" => \$play_prog, - "stop_prog=s" => \$stop_prog, - "l|volume=s" => \$volume, - "h|help" => \$help); - -sub usage { - print "ShairPort version $shairportversion - Airport Express emulator\n". - "Usage:\n". - basename($0) . " [OPTION...]\n". - "\n". - "Options:\n". - " -a, --apname=AirPort Sets AirPort name\n". - " -p, --password=secret Sets password\n", - " -o, --server_port=5002 Sets Port for Avahi/dns-sd/howl\n", - " -i, --pipe=pipepath Sets the path to a named pipe for output\n", - " --ao_driver=driver Sets the ao driver (optional)\n", - " --ao_devicename=devicename Sets the ao device name (optional)\n", - " --ao_deviceid=id Sets the ao device id (optional)\n", - " -s --squeezebox Enables local Squeezebox Server integration\n", - " -c --cliport=port Sets the SBS CLI port\n", - " -m --mac=address Sets the SB target device\n", - " -l --volume=level Sets the SB volume level (in %)\n", - " --play_prog=cmdline Program to start on 1st connection\n", - " --stop_prog=cmdline Program to start on last disconnection\n", - " -d Daemon mode\n", - " -w --writepid=path Write PID to this location\n", - " -v --verbose Print debugging messages\n", - " -h, --help This help\n", - "\n"; - exit; -} - -if (defined($help) && $help == 1) { usage(); } -# ensure that $verbose is set, one way or another... -if (defined($verbose) && $verbose) { - $verbose = 1; -} else { - $verbose = 0; -} - -$cliport = ( (defined( $cliport ) && $cliport && $cliport =~ m/^[0-9]+/ && $cliport > 1023 && $cliport < 65536 ) ? $cliport : 9090 ); - -$volume = ( (defined( $volume ) && $volume && $volume =~ m/^[0-9]+/ && $volume >= 0 && $volume <= 101 ) ? $volume : undef ); - -if (defined($squeeze) && $squeeze) { - my $players; - my @details; - - my $response; - my $socket = IO::Socket::INET -> new ( - PeerAddr => "127.0.0.1" - , PeerPort => $cliport - , Proto => 'tcp' - , Timeout => 1 - ); - if( !( $socket ) ) { - print "ERROR: Could not create socket to interface with SqueezeBox Server on port $cliport: $!\n"; - print "WARN: Disabling Squeezebox Server integration\n"; - undef $squeeze; - } - if( $squeeze ) { - print $socket "player count ?\n"; - $response = <$socket>; - if( !( defined( $response ) ) ) { - print "ERROR: Could not communicate with SqueezeBox Server on port $cliport\n"; - print "WARN: Disabling Squeezebox Server integration\n"; - undef $squeeze; - } else { - ( $players ) = ( $response =~ m/^player count ([0-9]+)$/ ); - print $socket "players 0 $players\n"; - $response = <$socket>; - @details = split( /playerindex%3A/ , $response ); - close( $socket ); - shift( @details ); - for( my $n = 0; $n <= scalar( @details ); $n++ ) { - if( defined( $details[ $n ] ) ) { - my $address = $details[ $n ]; - $address =~ s/^.*playerid%3A([[:xdigit:]%]+)\s.*$/$1/; - $address =~ s/%3A/:/g; - chomp $address; - $details[ $n ] = $address; - } - } - print "Discovered players: $players\n" if $verbose; - print "Player MAC addresses:\n" if $verbose; - foreach my $address (@details) { - print "\t$address\n" if( ( defined( $address ) && $address ) && $verbose ); - } - - if( defined( $mac ) && $mac ) { - chomp $mac; - if( !( grep { lc( $_ ) eq lc( $mac ) } @details ) ) { - print "ERROR: Invalid or non-present MAC specified.\n\n"; - print "Please select a target MAC address from:\n"; - foreach my $address (@details) { - print "\t$address\n" if( defined( $address ) && $address ); - } - exit(1); - } - } else { - if( 1 == $players ) { - $mac = $details[ 0 ]; - print "WARN: No Squeezebox player specified, using $mac.\n"; - } else { - print "ERROR: No Squeezebox player specified, please select a target MAC address with the '--mac' option with a value from:\n"; - foreach my $address (@details) { - print "\t$address\n" if( defined( $address ) && $address ); - } - exit(1); - } - } - $mac = uri_escape( $mac ) if( defined( $mac ) && $mac ); - } - } -}; -chomp $apname; - -my @hw_addr = +(map(ord, split(//, md5($apname))))[0..5]; - -sub POPE { - print "Broken pipe\n" if $verbose; - $SIG{PIPE} = \&POPE; -} -$SIG{PIPE} = \&POPE; - - -our $avahi_publish; -our $squeezebox_setup; - -sub REAP { - my $pid = waitpid( -1, WNOHANG ); - given( $pid ) { - when( $avahi_publish ) { - die( "avahi daemon terminated or 'avahi-publish-service' binary not found" ); - } - when( $squeezebox_setup ) { - print( "Squeezebox configuration routine completed\n" ) if $verbose; - } - } - print("Child exited\n") if $verbose; - $SIG{CHLD} = \&REAP; -}; -$SIG{CHLD} = \&REAP; - -my %conns; - -$SIG{TERM} = $SIG{INT} = sub { - print basename($0) . " killed\n"; - map { eval { kill $_->{decoder_pid} } } keys %conns; - kill 9, $avahi_publish if $avahi_publish; - # Clean up any running squeezebox_setup processes... - my $child; - do { - $child = waitpid( -1, WNOHANG ); - } while $child > 0; - exit 0; -}; -$SIG{__DIE__} = sub { - map { eval { kill $_->{decoder_pid} } } keys %conns; - kill 9, $avahi_publish if $avahi_publish; - # Clean up any running squeezebox_setup processes... - my $child; - do { - $child = waitpid( -1, WNOHANG ); - } while $child > 0; -}; - -$avahi_publish = fork(); -my $pw_clause = (length $password) ? "pw=true" : "pw=false"; -if ($avahi_publish==0) { - { exec 'avahi-publish-service', - join('', map { sprintf "%02X", $_ } @hw_addr) . "\@$apname", - "_raop._tcp", - $port, - "tp=UDP","sm=false","sv=false","ek=1","et=0,1","cn=0,1","ch=2","ss=16","sr=44100",$pw_clause,"vn=3","txtvers=1"; }; - { exec 'dns-sd', '-R', - join('', map { sprintf "%02X", $_ } @hw_addr) . "\@$apname", - "_raop._tcp", - ".", - $port, - "tp=UDP","sm=false","sv=false","ek=1","et=0,1","cn=0,1","ch=2","ss=16","sr=44100",$pw_clause,"vn=3","txtvers=1"; }; - { exec 'mDNSPublish', - join('', map { sprintf "%02X", $_ } @hw_addr) . "\@$apname", - "_raop._tcp", - $port, - "tp=UDP","sm=false","sv=false","ek=1","et=0,1","cn=0,1","ch=2","ss=16","sr=44100",$pw_clause,"vn=3","txtvers=1"; }; - die "could not run avahi-publish-service nor dns-sd nor mDNSPublish"; -} - -my $airport_pem = join '', ; -my $rsa = Crypt::OpenSSL::RSA->new_private_key($airport_pem) || die "RSA private key import failed"; - -my $listen; -{ - if (!defined($ipv4_only)) { - eval { - local $SIG{__DIE__}; - $listen = new IO::Socket::INET6(Listen => 1, - Domain => AF_INET6, - LocalPort => $port, - ReuseAddr => 1, - Proto => 'tcp'); - }; - if ($@) { - print "**************************************\n", - "* IO::Socket::INET6 not present! *\n", - "* Install this if iTunes won't play. *\n", - "**************************************\n\n"; - } - } - - $listen ||= new IO::Socket::INET(Listen => 1, - LocalPort => $port, - ReuseAddr => 1, - Proto => 'tcp'); -} -die "Can't listen on port " . $port . ": $!" unless $listen; - -sub ip6bin { - my $ip = shift; - $ip =~ /((.*)::)?(.+)/; - my @left = split /:/, $2; - my @right = split /:/, $3; - my @mid; - my $pad = 8 - ($#left + $#right + 2); - if ($pad > 0) { - @mid = (0) x $pad; - } - - pack('S>*', map { hex } (@left, @mid, @right)); -} - -my $sel = new IO::Select($listen); - -if ($daemon) { - chdir "/" or die "Could not chdir to '/': $!"; - umask 0; - open STDIN, "/dev/null" or die "Could not redirect /dev/null to STDIN(0): $!"; - open STDOUT, ">/dev/null" or die "Could not redirect STDOUT(1) to /dev/null: $!"; - defined( my $pid = fork() ) or die "Could not fork: $!"; - exit 0 if $pid; - setsid() or die "Could not start new session: $!"; - open STDERR, ">&STDOUT" or die "Could not dup STDOUT(1)"; -} -if (defined($writepid) && $writepid) { - open PID, ">$writepid" or die "Could not create PID file '$writepid': $!"; - print PID $$; - close PID; -} - -print "Listening...\n" if $verbose; - -sub performSqueezeboxSetup { - $squeezebox_setup = fork(); - if( 0 == $squeezebox_setup ) { - my $items; - my @favourites; - my @ids; - my $index; - - my $response; - - my $findFavourites = sub { - my ( $socket ) = @_; - print $socket "favorites items\n"; - $response = <$socket>; - $response =~ s/^\s*favorites\s+items\s+count%3A([0-9]+)\s*$/$1/; - $items = $response; - print "Found $items favourites...\n" if $verbose; - - print $socket "favorites items 0 $items want_url%3A1\n"; - $response = <$socket>; - undef( @favourites ); - @favourites = split( /id%3A/ , $response ); - @ids = split( /id%3A/ , $response ); - shift( @favourites ); - shift( @ids ); - for( my $n = 0; $n <= scalar( @favourites ); $n++ ) { - if( defined( $favourites[ $n ] ) ) { - my $url = $favourites[ $n ]; - $url =~ s/^.*url%3A([^ ]+)\s.*$/$1/; - $url = uri_unescape( $url ); - chomp $url; - print "\tFavourite with URL '$url' " if $verbose; - $favourites[ $n ] = $url; - } - if( defined( $ids[ $n ] ) ) { - my $id = $ids[ $n ]; - $id =~ s/^([^ ]+)\s.*$/$1/; - chomp $id; - my @components = split( /\./, $id ); - shift ( @components ); - $id = join( '.', @components ); - print "and ID '$id'\n" if $verbose; - $ids[ $n ] = $id; - } - } - }; - - my $socket = IO::Socket::INET -> new ( - PeerAddr => "127.0.0.1" - , PeerPort => ( (defined( $cliport ) and $cliport ) ? $cliport : 9090 ) - , Proto => 'tcp' - , Timeout => 1 - ) or die "Could not create socket: $!"; - - &$findFavourites( $socket ); - - print "Favourites URLs:\n" if $verbose; - foreach my $url (@favourites) { - print "\t$url\n" if( ( defined( $url ) && $url ) && $verbose ); - } - my $okay = 1; - if( !( grep { $_ =~ m/^wavin:/ } @favourites ) ) { - $okay = 0; - print "INFO: AirPlay 'wavin' Favourite does not exist - creating... "; - print $socket "favorites add url%3Awavin%3Aairplay title%3AAirPlay\n"; - $response = <$socket>; - if( $response =~ m/\scount%3A1/ ) { - print "done\nINFO: AirPlay 'wavin' favourite successfully created.\n"; - $okay = 1; - } else { - print "failed\nWARN: Could not create AirPlay favourite\n"; - print " Server response was $response\n"; - } - if( $okay ) { - &$findFavourites( $socket ); - - print "Updated Favourites URLs:\n" if $verbose; - foreach my $url (@favourites) { - print "\t$url\n" if( ( defined( $url ) && $url ) && $verbose ); - } - if( !( grep { $_ =~ m/^wavin:/ } @favourites ) ) { - print "WARN: Cloud not identify AirPlay Favourite, even after creating it - disabling SqueezeBox integration\n"; - $squeeze = 0; - $okay = 0; - } - } - } - if( $okay ) { - for ( my $n = 0 ; !( defined( $index ) ) && $n <= scalar( @favourites ) ; $n++ ) { - if( $favourites[ $n ] =~ m/^wavin:/ ) { - $index = $n; - print "Found favourite '" . $favourites[ $index ] . "' with ID '" . $ids[ $index ] . "' at position $index.\n" if $verbose; - } - } - - print "Turning on player (if off)... " if $verbose; - print $socket "$mac power 1\n"; - $response = <$socket>; - print "$response\n" if $verbose; - - print "Stopping player (if playing)... " if $verbose; - print $socket "$mac stop\n"; - $response = <$socket>; - print "$response\n" if $verbose; - - print "Unmuting player (if muted)... " if $verbose; - print $socket "$mac mixer muting 0\n"; - $response = <$socket>; - print "$response\n" if $verbose; - - if( defined( $volume ) ) { - print "Setting player volume to $volume... " if $verbose; - print $socket "$mac mixer volume $volume\n"; - $response = <$socket>; - print "$response\n" if $verbose; - } - - print "Showing message... " if $verbose; - print $socket "$mac show line2%3AStarting%20AirPlay duration%3A5 brightness%3ApowerOn font%3Ahuge\n"; - $response = <$socket>; - print "$response\n" if $verbose; - - if( defined( $index ) ) { - print "Playing favourite... " if $verbose; - my $id = uri_escape( $ids[ $index ] ); - print $socket "$mac favorites playlist play item_id%3A$id\n"; - } else { - print "Resuming play... " if $verbose; - print $socket "$mac play\n"; - } - $response = <$socket>; - print "$response\n" if $verbose; - } - close( $socket ); - - exit(0); - } -}; - -while (1) { - printf "about to select\n" if $verbose; - my @waiting = $sel->can_read; - foreach my $fh (@waiting) { - if ($fh==$listen) { - my $new = $listen->accept; - printf "New connection from %s\n", $new->peerhost if $verbose; - - $sel->add($new); - $new->blocking(0); - $conns{$new} = {fh => $fh}; - - if (defined($squeeze) && $squeeze) { - &performSqueezeboxSetup(); - } - - # the 2nd connection is a player connection - if (defined($play_prog) && $sel->count() == 2) { - printf "play prog: $play_prog\n" if ($verbose); - system($play_prog); - } - } else { - if (eof($fh)) { - print "Closed: $fh\n" if $verbose; - $sel->remove($fh); - close $fh; - # Prevent warnings when decoder_pid isn't defined - # (e.g. client connected, but playback not started) - if (defined($conns{$fh}{decoder_pid})) { - eval { kill $conns{$fh}{decoder_pid} }; - } - delete $conns{$fh}; - - # 1 connection means no connection - if (defined($stop_prog) && $sel->count() == 1) { - system($stop_prog); - } - next; - } - if (exists $conns{$fh}) { - conn_handle_data($fh); - } - } - } -} - -exit(1); # Unreachable - - -sub conn_handle_data { - my $fh = shift; - my $conn = $conns{$fh}; - - printf "handle data 1\n" if ($verbose); - - if ($conn->{req_need}) { - if (length($conn->{data}) >= $conn->{req_need}) { - $conn->{req}->content(substr($conn->{data}, 0, $conn->{req_need}, '')); - conn_handle_request($fh, $conn); - } - undef $conn->{req_need}; - return; - } - - read $fh, my $data, 4096; - $conn->{data} .= $data; - - if ($conn->{data} =~ /(\r\n\r\n|\n\n|\r\r)/) { - my $req_data = substr($conn->{data}, 0, $+[0], ''); - $conn->{req} = HTTP::Request->parse($req_data); - printf "REQ: %s\n", $conn->{req}->method if $verbose; - conn_handle_request($fh, $conn); - conn_handle_data($fh) if length($conn->{data}); - } -} - -sub digest_ok { - my ($req, $conn) = @_; - my $authz = $req->header('Authorization'); - return 0 unless $authz =~ s/^Digest\s+//i; - return 0 unless length $conn->{nonce}; - my @authz = split /,\s*/, $authz; - my %authz = map { /(.+)="(.+)"/; ($1, $2) } @authz; - - # not a standard digest - uses capital hex digits, in conflict with the RFC - my $digest = uc md5_hex ( - uc(md5_hex($authz{username} . ':' . $authz{realm} . ':' . $password)) - . ':' . $authz{nonce} . ':' . - uc(md5_hex($req->method . ':' . $authz{uri})) - ); - - return $digest eq $authz{response}; -} - -sub conn_handle_request { - my ($fh, $conn) = @_; - - my $req = $conn->{req};; - my $clen = $req->header('content-length') // 0; - if ($clen > 0 && !length($req->content)) { - $conn->{req_need} = $clen; - return; # need more! - } - - my $resp = HTTP::Response->new(200); - $resp->request($req); - $resp->protocol($req->protocol); - - $resp->header('CSeq', $req->header('CSeq')); - $resp->header('Audio-Jack-Status', 'connected; type=analog'); - - if (my $chall = $req->header('Apple-Challenge')) { - my $data = decode_base64($chall); - my $ip = $fh->sockhost; - if ($ip =~ /((\d+\.){3}\d+)$/) { # IPv4 - $data .= join '', map { chr } split(/\./, $1); - } else { - $data .= ip6bin($ip); - } - - $data .= join '', map { chr } @hw_addr; - $data .= chr(0) x (0x20-length($data)); - - $rsa->use_pkcs1_padding; # this isn't hashed before signing - my $signature = encode_base64 $rsa->private_encrypt($data), ''; - $signature =~ s/=*$//; - $resp->header('Apple-Response', $signature); - } - - if (length $password) { - if (!digest_ok($req, $conn)) { - my $nonce = md5_hex(map { rand } 1..20); - $conn->{nonce} = $nonce; - $resp->header('WWW-Authenticate', "Digest realm=\"$apname\", nonce=\"$nonce\""); - $resp->code(401); - $req->method('DENIED'); - } - } - - for ($req->method) { - /^OPTIONS$/ && do { - $resp->header('Public', 'ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER'); - last; - }; - - /^ANNOUNCE$/ && do { - my $sdp = Net::SDP->new($req->content); - my $audio = $sdp->media_desc_of_type('audio'); - - print $audio->as_string(); - print $audio->attribute('aesiv'); - - die("no AESIV") unless my $aesiv = decode_base64($audio->attribute('aesiv')); - die("no AESKEY") unless my $rsaaeskey = decode_base64($audio->attribute('rsaaeskey')); - $rsa->use_pkcs1_oaep_padding; - my $aeskey = $rsa->decrypt($rsaaeskey) || die "RSA decrypt failed"; - - $conn->{aesiv} = $aesiv; - $conn->{aeskey} = $aeskey; - $conn->{fmtp} = $audio->attribute('fmtp'); - last; - }; - - /^SETUP$/ && do { - my $transport = $req->header('Transport'); - $transport =~ s/;control_port=(\d+)//; - my $cport = $1; - $transport =~ s/;timing_port=(\d+)//; - my $tport = $1; - $transport =~ s/;server_port=(\d+)//; - my $dport = $1; - $resp->header('Session', 'DEADBEEF'); - - my %dec_args = ( - iv => unpack('H*', $conn->{aesiv}), - key => unpack('H*', $conn->{aeskey}), - fmtp => $conn->{fmtp}, - cport => $cport, - tport => $tport, - dport => $dport, -# host => 'unused', - ); - $dec_args{pipe} = $pipepath if defined $pipepath; - $dec_args{ao_driver} = $libao_driver if defined $libao_driver; - $dec_args{ao_devicename} = $libao_devicename if defined $libao_devicename; - $dec_args{ao_deviceid} = $libao_deviceid if defined $libao_deviceid; - - my $dec = '"' . $hairtunes_cli . '"' . join(' ', '', map { sprintf "%s '%s'", $_, $dec_args{$_} } keys(%dec_args)); - - if ($ipv4_only) { - $dec .= " ipv4_only"; - } - - print "decode command: $dec\n" if ($verbose); - my $decoder = open2(my $dec_out, my $dec_in, $dec); - - $conn->{decoder_pid} = $decoder; - $conn->{decoder_fh} = $dec_in; - my $portdesc = <$dec_out>; - die("Expected port number from decoder; got $portdesc") unless $portdesc =~ /^port: (\d+)/; - my $port = $1; - print "launched decoder: $decoder on port: $port\n" if $verbose; - $resp->header('Transport', $req->header('Transport') . ";server_port=$port"); - last; - }; - - /^RECORD$/ && last; - /^FLUSH$/ && do { - my $dfh = $conn->{decoder_fh}; - print $dfh "flush\n"; - last; - }; - /^TEARDOWN$/ && do { - $resp->header('Connection', 'close'); - close $conn->{decoder_fh}; - last; - }; - /^SET_PARAMETER$/ && do { - my @lines = split /[\r\n]+/, $req->content; - printf("SET_PARAMETER req: " . $req->content . "\n") if ($verbose); - my %content = map { /^(\S+): (.+)/; (lc $1, $2) } @lines; - my $cfh = $conn->{decoder_fh}; - if (exists $content{volume}) { - printf("sending-> vol: %f\n", $content{volume}) if ($verbose); - printf $cfh "vol: %f\n", $content{volume}; - } else { - printf("unable to perform content for req: " . $req->content . "\n") if ($verbose); - - } - last; - }; - /^GET_PARAMETER$/ && last; - /^DENIED$/ && last; - die("Unknown method: $_"); - } - - printf("%s", $resp->as_string("\r\n")) if ($verbose); - print $fh $resp->as_string("\r\n"); - $fh->flush; -} - -__DATA__ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA59dE8qLieItsH1WgjrcFRKj6eUWqi+bGLOX1HL3U3GhC/j0Qg90u3sG/1CUt -wC5vOYvfDmFI6oSFXi5ELabWJmT2dKHzBJKa3k9ok+8t9ucRqMd6DZHJ2YCCLlDRKSKv6kDqnw4U -wPdpOMXziC/AMj3Z/lUVX1G7WSHCAWKf1zNS1eLvqr+boEjXuBOitnZ/bDzPHrTOZz0Dew0uowxf -/+sG+NCK3eQJVxqcaJ/vEHKIVd2M+5qL71yJQ+87X6oV3eaYvt3zWZYD6z5vYTcrtij2VZ9Zmni/ -UAaHqn9JdsBWLUEpVviYnhimNVvYFZeCXg/IdTQ+x4IRdiXNv5hEewIDAQABAoIBAQDl8Axy9XfW -BLmkzkEiqoSwF0PsmVrPzH9KsnwLGH+QZlvjWd8SWYGN7u1507HvhF5N3drJoVU3O14nDY4TFQAa -LlJ9VM35AApXaLyY1ERrN7u9ALKd2LUwYhM7Km539O4yUFYikE2nIPscEsA5ltpxOgUGCY7b7ez5 -NtD6nL1ZKauw7aNXmVAvmJTcuPxWmoktF3gDJKK2wxZuNGcJE0uFQEG4Z3BrWP7yoNuSK3dii2jm -lpPHr0O/KnPQtzI3eguhe0TwUem/eYSdyzMyVx/YpwkzwtYL3sR5k0o9rKQLtvLzfAqdBxBurciz -aaA/L0HIgAmOit1GJA2saMxTVPNhAoGBAPfgv1oeZxgxmotiCcMXFEQEWflzhWYTsXrhUIuz5jFu -a39GLS99ZEErhLdrwj8rDDViRVJ5skOp9zFvlYAHs0xh92ji1E7V/ysnKBfsMrPkk5KSKPrnjndM -oPdevWnVkgJ5jxFuNgxkOLMuG9i53B4yMvDTCRiIPMQ++N2iLDaRAoGBAO9v//mU8eVkQaoANf0Z -oMjW8CN4xwWA2cSEIHkd9AfFkftuv8oyLDCG3ZAf0vrhrrtkrfa7ef+AUb69DNggq4mHQAYBp7L+ -k5DKzJrKuO0r+R0YbY9pZD1+/g9dVt91d6LQNepUE/yY2PP5CNoFmjedpLHMOPFdVgqDzDFxU8hL -AoGBANDrr7xAJbqBjHVwIzQ4To9pb4BNeqDndk5Qe7fT3+/H1njGaC0/rXE0Qb7q5ySgnsCb3DvA -cJyRM9SJ7OKlGt0FMSdJD5KG0XPIpAVNwgpXXH5MDJg09KHeh0kXo+QA6viFBi21y340NonnEfdf -54PX4ZGS/Xac1UK+pLkBB+zRAoGAf0AY3H3qKS2lMEI4bzEFoHeK3G895pDaK3TFBVmD7fV0Zhov -17fegFPMwOII8MisYm9ZfT2Z0s5Ro3s5rkt+nvLAdfC/PYPKzTLalpGSwomSNYJcB9HNMlmhkGzc -1JnLYT4iyUyx6pcZBmCd8bD0iwY/FzcgNDaUmbX9+XDvRA0CgYEAkE7pIPlE71qvfJQgoA9em0gI -LAuE4Pu13aKiJnfft7hIjbK+5kyb3TysZvoyDnb3HOKvInK7vXbKuU4ISgxB2bB3HcYzQMGsz1qJ -2gG0N5hvJpzwwhbhXqFKA4zaaSrw622wDniAK5MlIE0tIAKKP4yxNGjoD2QYjhBGuhvkWKY= ------END RSA PRIVATE KEY----- diff --git a/socketlib.c b/socketlib.c deleted file mode 100644 index 9efdf221..00000000 --- a/socketlib.c +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Socket Library - Common socket functions, based on stevens networking book. - * Copyright (c) M. Andrew Webster 2011 - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ - - -#include -#include "socketlib.h" -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -int common_setup(struct addrinfo *pAddrInfo) -{ - int tSock; - //printAddrs(pAddrInfo); - tSock = socket(pAddrInfo->ai_family, pAddrInfo->ai_socktype, 0); -#ifdef AF_INET6 - if((tSock==-1) && (pAddrInfo->ai_family == AF_INET6) && (errno == EAFNOSUPPORT)) - { - //Fallback to ipv4 - perror("Failed to create ipv6 socket. Trying ipv4"); - pAddrInfo->ai_family = AF_INET; - tSock = socket(pAddrInfo->ai_family, pAddrInfo->ai_socktype, 0); - } -#endif - return tSock; -} - -int setup_client(struct addrinfo *server_host) -{ - int tSockDesc = -1; - int tIdx = 0; - - while(tIdx++ < RETRY_COUNT) - { - tSockDesc = common_setup(server_host); - if (tSockDesc < 0 && tIdx >= RETRY_COUNT) - { - perror("Error: Could not create socket"); - return ERROR; - } - - if (connect(tSockDesc, server_host->ai_addr, server_host->ai_addrlen) >= 0) - { - return tSockDesc; - } - else - { - close(tSockDesc); - perror("Error: Could not connect to server"); - struct timeval tRes; - delay(RETRY_DELAY, &tRes); - } - } - printf("%d Retry attempts exceeded\n", RETRY_COUNT); - return ERROR; -} - -int getAddr(char *pHostname, char *pService, int pFamily, int pSockType, struct addrinfo **pAddrInfo) -{ - struct addrinfo hints; - int tError = 0; - memset(&hints, 0 , sizeof(hints)); - - hints.ai_family = pFamily; - hints.ai_socktype = pSockType; - if(pHostname == NULL) - { - hints.ai_flags = AI_PASSIVE; - } - - tError = getaddrinfo(pHostname, pService, &hints, pAddrInfo); - if(tError != 0) - { - printf("Error getting address info\n"); - } - return tError; -} - -int setup_server(struct addrinfo *server_addr) -{ - int tSock = common_setup(server_addr); - if (tSock < 0) - { - perror("Error: Could not create server socket"); - return ERROR; - } - - int tEnable = 1; - setsockopt(tSock, SOL_SOCKET, SO_REUSEADDR, &tEnable, sizeof (tEnable)); - if (bind(tSock, server_addr->ai_addr, server_addr->ai_addrlen) < 0) - { - close(tSock); - perror("Error: Could not bind socket"); - return ERROR; - } - - if (listen(tSock, 5) < 0) - { - close(tSock); - perror("Error: Unable to listen on server socket"); - return ERROR; - } - - return tSock; -} - - -int acceptClient(int pSock, struct addrinfo *server_addr) -{ - int tAccept = accept(pSock, server_addr->ai_addr, &server_addr->ai_addrlen); - // close the listen socket. Not expecting any more clients. - if (tAccept < 0) - { - perror("Error: Unable to accept connection to server socket"); - return ERROR; - } - else - { - //printf("..Accepted on socket: %d\n", tAccept); - } - return tAccept; -} - -int setupListenServer(struct addrinfo **pAddrInfo, int pPort) -{ - char tService[SERVLEN]; - sprintf(tService, "%d", pPort); // copies port to string - int tFamily = AF_INET; - #ifdef AF_INET6 - //printf("Listening on IPv6 Socket\n"); - tFamily = AF_INET6; - #else - //printf("Listening on IPv4 Socket"); - #endif - if(getAddr(NULL, tService, tFamily, SOCK_STREAM, pAddrInfo)) - { - return ERROR; // getAddr prints out error message - } - - int tSocketDescriptor = setup_server(*pAddrInfo); - char tAddr[INET6_ADDRSTRLEN]; - socklen_t tSize = INET6_ADDRSTRLEN; - inet_ntop((*pAddrInfo)->ai_family, (*pAddrInfo)->ai_addr, tAddr, tSize); - //printf("Size is: %d\n", tSize); - return tSocketDescriptor; -} - -void delay(long pMillisecs, struct timeval *pRes) -{ - pRes->tv_sec = pMillisecs / 1000; - pRes->tv_usec = (pMillisecs - (pRes->tv_sec * 1000)) * 1000; - select(0,NULL,NULL,NULL,pRes); -} - -static int getCorrectedEncodeSize(int pSize) -{ - if(pSize % 4 == 0) - { - return pSize; - } - else if((pSize + 1) % 4 == 0) - { - return pSize+1; - } - else if((pSize + 2) % 4 == 0) - { - return pSize+2; - } - else - { - // Invalid encoded data, no other cases are possible. - printf("Unrecoverable error....base64 values are incorrectly encoded\n"); - return pSize; - } -} - -// From http://www.ioncannon.net/programming/34/howto-base64-encode-with-cc-and-openssl/ - -//int main(int argc, char **argv) -//{ -// char *output = decode_base64("WU9ZTyEA\n\0", strlen("WU9ZTyEA\n\0")); -// printf("Unbase64: *%s*\n", output); -// free(output);/ -//} - -char *decode_base64(unsigned char *pInput, int pLength, int *pActualLength) -{ - // Needs All NO_NL flags for proper RSA AES KEY Descrypt - BIO *b64, *bmem; - unsigned char *input = pInput; - int length = getCorrectedEncodeSize(pLength); - if(pLength != length) - { - input = malloc(length * sizeof(unsigned char)); - memset(input, 0, length); - memcpy(input, pInput, pLength); - memset(input+pLength, '=', length-pLength); - printf("Fixed value: [%.*s]\n", length, input); - } - char *buffer = (char *)malloc(length); - memset(buffer, 0, length); - - b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - - bmem = BIO_new_mem_buf(input, length); - BIO_set_flags(bmem, BIO_FLAGS_BASE64_NO_NL); - bmem = BIO_push(b64, bmem); - - BIO_set_flags(bmem, BIO_FLAGS_BASE64_NO_NL); - - *pActualLength = BIO_read(bmem, buffer, length); - - BIO_free_all(bmem); - - if(pLength != length) - { - free(input); - } - return buffer; -} - -char *encode_base64(unsigned char *input, int length) -{ - BIO *bmem, *b64; - BUF_MEM *bptr; - - b64 = BIO_new(BIO_f_base64()); - // This enables/disables nls - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - bmem = BIO_new(BIO_s_mem()); - - b64 = BIO_push(b64, bmem); - - BIO_write(b64, input, length); - (void)BIO_flush(b64); - BIO_get_mem_ptr(b64, &bptr); - - char *buff = (char *)malloc(bptr->length); - memcpy(buff, bptr->data, bptr->length-1); - buff[bptr->length-1] = 0; - - BIO_free_all(b64); - - return buff; -} diff --git a/socketlib.h b/socketlib.h deleted file mode 100644 index 6d501f33..00000000 --- a/socketlib.h +++ /dev/null @@ -1,39 +0,0 @@ -/* Name: M. Andrew Webster - * Based on stevens networking book - * Description: function prototypes for socket library - */ - -#ifndef _SOCKETLIB_H -#define _SOCKETLIB_H - -#include -#include -#include -#include -#include - - -#define MAX_SIZE 8192 -#define PORT 5002 -#define RETRY_COUNT 7 -#define SERVLEN 80 -#define RETRY_DELAY 1000 - -#define ERROR -1 -#define DEFAULT_IP "127.0.0.1" -#define DEFAULT_UNIX "/unix" - -int setup_client(struct addrinfo *server_info); -int setup_server(struct addrinfo *server_address); -int setupListenServer(struct addrinfo **pAddrInfo, int pPort); -int acceptClient(int pSock, struct addrinfo *server_addr); -void delay(long pMillisecs, struct timeval *pRes); -int getAddr(char *pHostname, char *pService, int pFamily, int pSockType, struct addrinfo **pAddrInfo); - -// All calls to decode and encode need to be freed -char *decode_base64(unsigned char *input, int length, int *tActualLength); -// All calls to decode and encode need to be freed -char *encode_base64(unsigned char *input, int length); - - -#endif