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