owntone-server/src/outputs/raop.c
Scott Shambarger ce4ef0aa23 [config] Many updates to configuration and feature checks
- Added custom checks for libraries and pkgconfig modules that test library
 presence with additional checks for use of headers and functions with
 given options.  Also support correct additional feature library checks
 using provided flags.
- Added custom enable/disable feature macros to simplify their use.
- Use custom CFLAGS and LIBS variables for Makefiles to simplify maintenance.
- Update many feature checks from platform to function.
- Streamline many function checks.
- Correctly check gnutls, gcrypt and gpg-error libraries.
- Fix chromecast and spotify config and compile on FreeBSD
- Added inotify, signalfd and kqueue, and byte swap checks.
- Many clarifications of error messages.
- Correct json-c checks to properly use supplied CFLAGS.
- Correct many quoting inconsistencies
- Use __DATE__ in place of BUILDDATE
- Use full path for gperf and antlr3
- Remove unnecessary CFLAGS
- Added tests for pthread_setname_np parameters
- Added tests for clock_gettime and timer_settime
- Added tests for time.h
- Test if pthread, dl and rt libs are required/available.
- Updated checks for libunistring
2017-01-06 00:44:18 -08:00

4518 lines
100 KiB
C

/*
* Copyright (C) 2010-2011 Julien BLACHE <jb@jblache.org>
*
* RAOP AirTunes v2
*
* Crypto code adapted from VideoLAN
* Copyright (C) 2008 the VideoLAN team
* Author: Michael Hanselmann
* GPLv2+
*
* ALAC encoding adapted from raop_play
* Copyright (C) 2005 Shiro Ninomiya <shiron@snino.com>
* GPLv2+
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/* TODO:
* - Support RTSP authentication in all requests (only OPTIONS so far)
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <math.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <fcntl.h>
#include <time.h>
#ifdef HAVE_ENDIAN_H
# include <endian.h>
#elif defined(HAVE_SYS_ENDIAN_H)
# include <sys/endian.h>
#endif
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <event2/event.h>
#include <event2/buffer.h>
#include <gcrypt.h>
#include "evrtsp/evrtsp.h"
#include "conffile.h"
#include "logger.h"
#include "mdns.h"
#include "misc.h"
#include "player.h"
#include "db.h"
#include "artwork.h"
#include "dmap_common.h"
#include "outputs.h"
#ifndef MIN
# define MIN(a, b) ((a < b) ? a : b)
#endif
#define AIRTUNES_V2_HDR_LEN 12
#define ALAC_HDR_LEN 3
#define AIRTUNES_V2_PKT_LEN (AIRTUNES_V2_HDR_LEN + ALAC_HDR_LEN + STOB(AIRTUNES_V2_PACKET_SAMPLES))
#define AIRTUNES_V2_PKT_TAIL_LEN (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN - ((AIRTUNES_V2_PKT_LEN / 16) * 16))
#define AIRTUNES_V2_PKT_TAIL_OFF (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_PKT_TAIL_LEN)
#define RETRANSMIT_BUFFER_SIZE 1000
#define RAOP_MD_DELAY_STARTUP 15360
#define RAOP_MD_DELAY_SWITCH (RAOP_MD_DELAY_STARTUP * 2)
/* This is an arbitrary value which just needs to be kept in sync with the config */
#define RAOP_CONFIG_MAX_VOLUME 11
union sockaddr_all
{
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
struct sockaddr sa;
struct sockaddr_storage ss;
};
struct raop_v2_packet
{
uint8_t clear[AIRTUNES_V2_PKT_LEN];
uint8_t encrypted[AIRTUNES_V2_PKT_LEN];
uint16_t seqnum;
struct raop_v2_packet *prev;
struct raop_v2_packet *next;
};
enum raop_devtype {
RAOP_DEV_APEX1_80211G,
RAOP_DEV_APEX2_80211N,
RAOP_DEV_APEX3_80211N,
RAOP_DEV_APPLETV,
RAOP_DEV_APPLETV4,
RAOP_DEV_OTHER,
};
// Session is starting up
#define RAOP_STATE_F_STARTUP (1 << 14)
// Streaming is up (connection established)
#define RAOP_STATE_F_CONNECTED (1 << 15)
enum raop_state {
// Device is stopped (no session)
RAOP_STATE_STOPPED = 0,
// Session startup
RAOP_STATE_STARTUP = RAOP_STATE_F_STARTUP,
RAOP_STATE_OPTIONS = RAOP_STATE_F_STARTUP | 0x01,
RAOP_STATE_ANNOUNCE = RAOP_STATE_F_STARTUP | 0x02,
RAOP_STATE_SETUP = RAOP_STATE_F_STARTUP | 0x03,
RAOP_STATE_RECORD = RAOP_STATE_F_STARTUP | 0x04,
// Session established
// - streaming ready (RECORD sent and acked, connection established)
// - commands (SET_PARAMETER) are possible
RAOP_STATE_CONNECTED = RAOP_STATE_F_CONNECTED,
// Media data is being sent
RAOP_STATE_STREAMING = RAOP_STATE_F_CONNECTED | 0x01,
// Session is failed, couldn't startup or error occurred
RAOP_STATE_FAILED = -1,
// Password issue: unknown password or bad password
RAOP_STATE_PASSWORD = -2,
};
// Info about the device, which is not required by the player, only internally
struct raop_extra
{
enum raop_devtype devtype;
unsigned encrypt:1;
unsigned wants_metadata:1;
};
struct raop_session
{
struct evrtsp_connection *ctrl;
enum raop_state state;
unsigned req_has_auth:1;
unsigned encrypt:1;
unsigned auth_quirk_itunes:1;
unsigned wants_metadata:1;
unsigned keep_alive:1;
struct event *deferredev;
int reqs_in_flight;
int cseq;
char *session;
char session_url[128];
char *realm;
char *nonce;
const char *password;
char *devname;
char *address;
int volume;
uint64_t start_rtptime;
/* Do not dereference - only passed to the status cb */
struct output_device *device;
struct output_session *output_session;
output_status_cb status_cb;
/* AirTunes v2 */
unsigned short server_port;
unsigned short control_port;
unsigned short timing_port;
int server_fd;
union sockaddr_all sa;
struct raop_service *timing_svc;
struct raop_service *control_svc;
struct raop_session *next;
};
struct raop_metadata
{
struct evbuffer *metadata;
struct evbuffer *artwork;
int artwork_fmt;
/* Progress data */
uint64_t start;
uint64_t end;
struct raop_metadata *next;
};
struct raop_service
{
int fd;
unsigned short port;
struct event *ev;
};
typedef void (*evrtsp_req_cb)(struct evrtsp_request *req, void *arg);
/* Truncate RTP time to lower 32bits for RAOP */
#define RAOP_RTPTIME(x) ((uint32_t)((x) & (uint64_t)0xffffffff))
/* NTP timestamp definitions */
#define FRAC 4294967296. /* 2^32 as a double */
#define NTP_EPOCH_DELTA 0x83aa7e80 /* 2208988800 - that's 1970 - 1900 in seconds */
struct ntp_stamp
{
uint32_t sec;
uint32_t frac;
};
static const uint8_t raop_rsa_pubkey[] =
"\xe7\xd7\x44\xf2\xa2\xe2\x78\x8b\x6c\x1f\x55\xa0\x8e\xb7\x05\x44"
"\xa8\xfa\x79\x45\xaa\x8b\xe6\xc6\x2c\xe5\xf5\x1c\xbd\xd4\xdc\x68"
"\x42\xfe\x3d\x10\x83\xdd\x2e\xde\xc1\xbf\xd4\x25\x2d\xc0\x2e\x6f"
"\x39\x8b\xdf\x0e\x61\x48\xea\x84\x85\x5e\x2e\x44\x2d\xa6\xd6\x26"
"\x64\xf6\x74\xa1\xf3\x04\x92\x9a\xde\x4f\x68\x93\xef\x2d\xf6\xe7"
"\x11\xa8\xc7\x7a\x0d\x91\xc9\xd9\x80\x82\x2e\x50\xd1\x29\x22\xaf"
"\xea\x40\xea\x9f\x0e\x14\xc0\xf7\x69\x38\xc5\xf3\x88\x2f\xc0\x32"
"\x3d\xd9\xfe\x55\x15\x5f\x51\xbb\x59\x21\xc2\x01\x62\x9f\xd7\x33"
"\x52\xd5\xe2\xef\xaa\xbf\x9b\xa0\x48\xd7\xb8\x13\xa2\xb6\x76\x7f"
"\x6c\x3c\xcf\x1e\xb4\xce\x67\x3d\x03\x7b\x0d\x2e\xa3\x0c\x5f\xff"
"\xeb\x06\xf8\xd0\x8a\xdd\xe4\x09\x57\x1a\x9c\x68\x9f\xef\x10\x72"
"\x88\x55\xdd\x8c\xfb\x9a\x8b\xef\x5c\x89\x43\xef\x3b\x5f\xaa\x15"
"\xdd\xe6\x98\xbe\xdd\xf3\x59\x96\x03\xeb\x3e\x6f\x61\x37\x2b\xb6"
"\x28\xf6\x55\x9f\x59\x9a\x78\xbf\x50\x06\x87\xaa\x7f\x49\x76\xc0"
"\x56\x2d\x41\x29\x56\xf8\x98\x9e\x18\xa6\x35\x5b\xd8\x15\x97\x82"
"\x5e\x0f\xc8\x75\x34\x3e\xc7\x82\x11\x76\x25\xcd\xbf\x98\x44\x7b";
static const uint8_t raop_rsa_exp[] = "\x01\x00\x01";
/* Keep in sync with enum raop_devtype */
static const char *raop_devtype[] =
{
"AirPort Express 1 - 802.11g",
"AirPort Express 2 - 802.11n",
"AirPort Express 3 - 802.11n",
"AppleTV",
"AppleTV4",
"Other",
};
/* From player.c */
extern struct event_base *evbase_player;
/* RAOP AES stream key */
static uint8_t raop_aes_key[16];
static uint8_t raop_aes_iv[16];
static gcry_cipher_hd_t raop_aes_ctx;
/* Base64-encoded AES key and IV for SDP */
static char *raop_aes_key_b64;
static char *raop_aes_iv_b64;
/* AirTunes v2 time synchronization */
static struct raop_service timing_4svc;
static struct raop_service timing_6svc;
/* AirTunes v2 playback synchronization / control */
static struct raop_service control_4svc;
static struct raop_service control_6svc;
static int sync_counter;
/* AirTunes v2 audio stream */
static uint32_t ssrc_id;
static uint16_t stream_seq;
/* Retransmit packet buffer */
static int pktbuf_size;
static struct raop_v2_packet *pktbuf_head;
static struct raop_v2_packet *pktbuf_tail;
/* Metadata */
static struct raop_metadata *metadata_head;
static struct raop_metadata *metadata_tail;
/* FLUSH timer */
static struct event *flush_timer;
/* Keep-alive timer - hack for ATV's with tvOS 10 */
static struct event *keep_alive_timer;
static struct timeval keep_alive_tv = { 60, 0 };
/* Sessions */
static struct raop_session *sessions;
/* ALAC bits writer - big endian
* p outgoing buffer pointer
* val bitfield value
* blen bitfield length, max 8 bits
* bpos bit position in the current byte (pointed by *p)
*/
static inline void
alac_write_bits(uint8_t **p, uint8_t val, int blen, int *bpos)
{
int lb;
int rb;
int bd;
/* Remaining bits in the current byte */
lb = 7 - *bpos + 1;
/* Number of bits overflowing */
rb = lb - blen;
if (rb >= 0)
{
bd = val << rb;
if (*bpos == 0)
**p = bd;
else
**p |= bd;
/* No over- nor underflow, we're done with this byte */
if (rb == 0)
{
*p += 1;
*bpos = 0;
}
else
*bpos += blen;
}
else
{
/* Fill current byte */
bd = val >> -rb;
**p |= bd;
/* Overflow goes to the next byte */
*p += 1;
**p = val << (8 + rb);
*bpos = -rb;
}
}
/* Raw data must be little endian */
static void
alac_encode(uint8_t *raw, uint8_t *buf, int buflen)
{
uint8_t *maxraw;
int bpos;
bpos = 0;
maxraw = raw + buflen;
alac_write_bits(&buf, 1, 3, &bpos); /* channel=1, stereo */
alac_write_bits(&buf, 0, 4, &bpos); /* unknown */
alac_write_bits(&buf, 0, 8, &bpos); /* unknown */
alac_write_bits(&buf, 0, 4, &bpos); /* unknown */
alac_write_bits(&buf, 0, 1, &bpos); /* hassize */
alac_write_bits(&buf, 0, 2, &bpos); /* unused */
alac_write_bits(&buf, 1, 1, &bpos); /* is-not-compressed */
for (; raw < maxraw; raw += 4)
{
/* Byteswap to big endian */
alac_write_bits(&buf, *(raw + 1), 8, &bpos);
alac_write_bits(&buf, *raw, 8, &bpos);
alac_write_bits(&buf, *(raw + 3), 8, &bpos);
alac_write_bits(&buf, *(raw + 2), 8, &bpos);
}
}
/* AirTunes v2 time synchronization helpers */
static inline void
timespec_to_ntp(struct timespec *ts, struct ntp_stamp *ns)
{
/* Seconds since NTP Epoch (1900-01-01) */
ns->sec = ts->tv_sec + NTP_EPOCH_DELTA;
ns->frac = (uint32_t)((double)ts->tv_nsec * 1e-9 * FRAC);
}
static inline void
ntp_to_timespec(struct ntp_stamp *ns, struct timespec *ts)
{
/* Seconds since Unix Epoch (1970-01-01) */
ts->tv_sec = ns->sec - NTP_EPOCH_DELTA;
ts->tv_nsec = (long)((double)ns->frac / (1e-9 * FRAC));
}
static inline int
raop_v2_timing_get_clock_ntp(struct ntp_stamp *ns)
{
struct timespec ts;
int ret;
ret = clock_gettime(CLOCK_MONOTONIC, &ts);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't get clock: %s\n", strerror(errno));
return -1;
}
timespec_to_ntp(&ts, ns);
return 0;
}
/* RAOP crypto stuff - from VLC */
/* MGF1 is specified in RFC2437, section 10.2.1. Variables are named after the
* specification.
*/
static int
raop_crypt_mgf1(uint8_t *mask, size_t l, const uint8_t *z, const size_t zlen, const int hash)
{
char ebuf[64];
gcry_md_hd_t md_hdl;
gpg_error_t gc_err;
uint8_t *md;
uint32_t counter;
uint8_t c[4];
size_t copylen;
int len;
gc_err = gcry_md_open(&md_hdl, hash, 0);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not open hash: %s\n", ebuf);
return -1;
}
len = gcry_md_get_algo_dlen(hash);
counter = 0;
while (l > 0)
{
/* 3. For counter from 0 to \lceil{l / len}\rceil-1, do the following:
* a. Convert counter to an octet string C of length 4 with the
* primitive I2OSP: C = I2OSP (counter, 4)
*/
c[0] = (counter >> 24) & 0xff;
c[1] = (counter >> 16) & 0xff;
c[2] = (counter >> 8) & 0xff;
c[3] = counter & 0xff;
++counter;
/* b. Concatenate the hash of the seed z and c to the octet string T:
* T = T || Hash (Z || C)
*/
gcry_md_reset(md_hdl);
gcry_md_write(md_hdl, z, zlen);
gcry_md_write(md_hdl, c, 4);
md = gcry_md_read(md_hdl, hash);
/* 4. Output the leading l octets of T as the octet string mask. */
copylen = MIN(l, len);
memcpy(mask, md, copylen);
mask += copylen;
l -= copylen;
}
gcry_md_close(md_hdl);
return 0;
}
/* EME-OAEP-ENCODE is specified in RFC2437, section 9.1.1.1. Variables are
* named after the specification.
*/
static int
raop_crypt_add_oaep_padding(uint8_t *em, const size_t emlen, const uint8_t *m, const size_t mlen, const uint8_t *p, const size_t plen)
{
uint8_t *seed;
uint8_t *db;
uint8_t *db_mask;
uint8_t *seed_mask;
size_t emlen_max;
size_t pslen;
size_t i;
int hlen;
int ret;
/* Space for 0x00 prefix in EM. */
emlen_max = emlen - 1;
hlen = gcry_md_get_algo_dlen(GCRY_MD_SHA1);
/* Step 2:
* If ||M|| > emLen-2hLen-1 then output "message too long" and stop.
*/
if (mlen > (emlen_max - (2 * hlen) - 1))
{
DPRINTF(E_LOG, L_RAOP, "Could not add OAEP padding: message too long\n");
return -1;
}
/* Step 3:
* Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero
* octets. The length of PS may be 0.
*/
pslen = emlen_max - mlen - (2 * hlen) - 1;
/*
* Step 5:
* Concatenate pHash, PS, the message M, and other padding to form a data
* block DB as: DB = pHash || PS || 01 || M
*/
db = calloc(1, hlen + pslen + 1 + mlen);
db_mask = calloc(1, emlen_max - hlen);
seed_mask = calloc(1, hlen);
if (!db || !db_mask || !seed_mask)
{
DPRINTF(E_LOG, L_RAOP, "Could not allocate memory for OAEP padding\n");
if (db)
free(db);
if (db_mask)
free(db_mask);
if (seed_mask)
free(seed_mask);
return -1;
}
/* Step 4:
* Let pHash = Hash(P), an octet string of length hLen.
*/
gcry_md_hash_buffer(GCRY_MD_SHA1, db, p, plen);
/* Step 3:
* Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero
* octets. The length of PS may be 0.
*/
memset(db + hlen, 0, pslen);
/* Step 5:
* Concatenate pHash, PS, the message M, and other padding to form a data
* block DB as: DB = pHash || PS || 01 || M
*/
db[hlen + pslen] = 0x01;
memcpy(db + hlen + pslen + 1, m, mlen);
/* Step 6:
* Generate a random octet string seed of length hLen
*/
seed = gcry_random_bytes(hlen, GCRY_STRONG_RANDOM);
if (!seed)
{
DPRINTF(E_LOG, L_RAOP, "Could not allocate memory for OAEP seed\n");
ret = -1;
goto out_free_alloced;
}
/* Step 7:
* Let dbMask = MGF(seed, emLen-hLen).
*/
ret = raop_crypt_mgf1(db_mask, emlen_max - hlen, seed, hlen, GCRY_MD_SHA1);
if (ret < 0)
goto out_free_all;
/* Step 8:
* Let maskedDB = DB \xor dbMask.
*/
for (i = 0; i < (emlen_max - hlen); i++)
db[i] ^= db_mask[i];
/* Step 9:
* Let seedMask = MGF(maskedDB, hLen).
*/
ret = raop_crypt_mgf1(seed_mask, hlen, db, emlen_max - hlen, GCRY_MD_SHA1);
if (ret < 0)
goto out_free_all;
/* Step 10:
* Let maskedSeed = seed \xor seedMask.
*/
for (i = 0; i < hlen; i++)
seed[i] ^= seed_mask[i];
/* Step 11:
* Let EM = maskedSeed || maskedDB.
*/
em[0] = 0x00;
memcpy(em + 1, seed, hlen);
memcpy(em + 1 + hlen, db, hlen + pslen + 1 + mlen);
/* Step 12:
* Output EM.
*/
ret = 0;
out_free_all:
free(seed);
out_free_alloced:
free(db);
free(db_mask);
free(seed_mask);
return ret;
}
static char *
raop_crypt_encrypt_aes_key_base64(void)
{
char ebuf[64];
uint8_t padded_key[256];
gpg_error_t gc_err;
gcry_sexp_t sexp_rsa_params;
gcry_sexp_t sexp_input;
gcry_sexp_t sexp_encrypted;
gcry_sexp_t sexp_token_a;
gcry_mpi_t mpi_pubkey;
gcry_mpi_t mpi_exp;
gcry_mpi_t mpi_input;
gcry_mpi_t mpi_output;
char *result;
uint8_t *value;
size_t value_size;
int ret;
result = NULL;
/* Add RSA-OAES-SHA1 padding */
ret = raop_crypt_add_oaep_padding(padded_key, sizeof(padded_key), raop_aes_key, sizeof(raop_aes_key), NULL, 0);
if (ret < 0)
return NULL;
/* Read public key */
gc_err = gcry_mpi_scan(&mpi_pubkey, GCRYMPI_FMT_USG, raop_rsa_pubkey, sizeof(raop_rsa_pubkey) - 1, NULL);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not read RAOP RSA pubkey: %s\n", ebuf);
return NULL;
}
/* Read exponent */
gc_err = gcry_mpi_scan(&mpi_exp, GCRYMPI_FMT_USG, raop_rsa_exp, sizeof(raop_rsa_exp) - 1, NULL);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not read RAOP RSA exponent: %s\n", ebuf);
goto out_free_mpi_pubkey;
}
/* If the input data starts with a set bit (0x80), gcrypt thinks it's a
* signed integer and complains. Prefixing it with a zero byte (\0)
* works, but involves more work. Converting it to an MPI in our code is
* cleaner.
*/
gc_err = gcry_mpi_scan(&mpi_input, GCRYMPI_FMT_USG, padded_key, sizeof(padded_key), NULL);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not convert input data: %s\n", ebuf);
goto out_free_mpi_exp;
}
/* Build S-expression with RSA parameters */
gc_err = gcry_sexp_build(&sexp_rsa_params, NULL, "(public-key(rsa(n %m)(e %m)))", mpi_pubkey, mpi_exp);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not build RSA params S-exp: %s\n", ebuf);
goto out_free_mpi_input;
}
/* Build S-expression for data */
gc_err = gcry_sexp_build(&sexp_input, NULL, "(data(value %m))", mpi_input);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not build data S-exp: %s\n", ebuf);
goto out_free_sexp_params;
}
/* Encrypt data */
gc_err = gcry_pk_encrypt(&sexp_encrypted, sexp_input, sexp_rsa_params);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not encrypt data: %s\n", ebuf);
goto out_free_sexp_input;
}
/* Extract encrypted data */
sexp_token_a = gcry_sexp_find_token(sexp_encrypted, "a", 0);
if (!sexp_token_a)
{
DPRINTF(E_LOG, L_RAOP, "Could not find token 'a' in result S-exp\n");
goto out_free_sexp_encrypted;
}
mpi_output = gcry_sexp_nth_mpi(sexp_token_a, 1, GCRYMPI_FMT_USG);
if (!mpi_output)
{
DPRINTF(E_LOG, L_RAOP, "Cannot extract MPI from result\n");
goto out_free_sexp_token_a;
}
/* Copy encrypted data into char array */
gc_err = gcry_mpi_aprint(GCRYMPI_FMT_USG, &value, &value_size, mpi_output);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not copy encrypted data: %s\n", ebuf);
goto out_free_mpi_output;
}
/* Encode in Base64 */
result = b64_encode(value, value_size);
free(value);
out_free_mpi_output:
gcry_mpi_release(mpi_output);
out_free_sexp_token_a:
gcry_sexp_release(sexp_token_a);
out_free_sexp_encrypted:
gcry_sexp_release(sexp_encrypted);
out_free_sexp_input:
gcry_sexp_release(sexp_input);
out_free_sexp_params:
gcry_sexp_release(sexp_rsa_params);
out_free_mpi_input:
gcry_mpi_release(mpi_input);
out_free_mpi_exp:
gcry_mpi_release(mpi_exp);
out_free_mpi_pubkey:
gcry_mpi_release(mpi_pubkey);
return result;
}
/* RAOP metadata */
static void
raop_metadata_free(struct raop_metadata *rmd)
{
evbuffer_free(rmd->metadata);
if (rmd->artwork)
evbuffer_free(rmd->artwork);
free(rmd);
}
static void
raop_metadata_purge(void)
{
struct raop_metadata *rmd;
for (rmd = metadata_head; rmd; rmd = metadata_head)
{
metadata_head = rmd->next;
raop_metadata_free(rmd);
}
metadata_tail = NULL;
}
static void
raop_metadata_prune(uint64_t rtptime)
{
struct raop_metadata *rmd;
for (rmd = metadata_head; rmd; rmd = metadata_head)
{
if (rmd->end >= rtptime)
break;
if (metadata_tail == metadata_head)
metadata_tail = rmd->next;
metadata_head = rmd->next;
raop_metadata_free(rmd);
}
}
/* Thread: worker */
static void *
raop_metadata_prepare(int id)
{
struct db_queue_item *queue_item;
struct raop_metadata *rmd;
struct evbuffer *tmp;
int ret;
rmd = (struct raop_metadata *)malloc(sizeof(struct raop_metadata));
if (!rmd)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP metadata\n");
return NULL;
}
memset(rmd, 0, sizeof(struct raop_metadata));
queue_item = db_queue_fetch_byitemid(id);
if (!queue_item)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for queue item\n");
goto out_rmd;
}
/* Get artwork */
rmd->artwork = evbuffer_new();
if (!rmd->artwork)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n");
goto skip_artwork;
}
ret = artwork_get_item(rmd->artwork, queue_item->file_id, 600, 600);
if (ret < 0)
{
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", id);
evbuffer_free(rmd->artwork);
rmd->artwork = NULL;
}
rmd->artwork_fmt = ret;
skip_artwork:
/* Turn it into DAAP metadata */
tmp = evbuffer_new();
if (!tmp)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for temporary metadata evbuffer; metadata will not be sent\n");
goto out_qi;
}
rmd->metadata = evbuffer_new();
if (!rmd->metadata)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for metadata evbuffer; metadata will not be sent\n");
evbuffer_free(tmp);
goto out_qi;
}
ret = dmap_encode_queue_metadata(rmd->metadata, tmp, queue_item);
evbuffer_free(tmp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n");
goto out_metadata;
}
/* Progress - raop_metadata_send() will add rtptime to these */
rmd->start = 0;
rmd->end = (queue_item->song_length * 44100UL) / 1000UL;
free_queue_item(queue_item, 0);
return rmd;
out_metadata:
evbuffer_free(rmd->metadata);
out_qi:
free_queue_item(queue_item, 0);
out_rmd:
free(rmd);
return NULL;
}
/* Helpers */
static int
raop_add_auth(struct raop_session *rs, struct evrtsp_request *req, const char *method, const char *uri)
{
char ha1[33];
char ha2[33];
char ebuf[64];
char auth[256];
const char *hash_fmt;
const char *username;
uint8_t *hash_bytes;
size_t hashlen;
gcry_md_hd_t hd;
gpg_error_t gc_err;
int i;
int ret;
rs->req_has_auth = 0;
if (!rs->nonce)
return 0;
if (!rs->password)
{
DPRINTF(E_LOG, L_RAOP, "Authentication required but no password found for device '%s'\n", rs->devname);
return -2;
}
if (rs->auth_quirk_itunes)
{
hash_fmt = "%02X"; /* Uppercase hex */
username = "iTunes";
}
else
{
hash_fmt = "%02x";
username = ""; /* No username */
}
gc_err = gcry_md_open(&hd, GCRY_MD_MD5, 0);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not open MD5: %s\n", ebuf);
return -1;
}
memset(ha1, 0, sizeof(ha1));
memset(ha2, 0, sizeof(ha2));
hashlen = gcry_md_get_algo_dlen(GCRY_MD_MD5);
/* HA 1 */
gcry_md_write(hd, username, strlen(username));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, rs->realm, strlen(rs->realm));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, rs->password, strlen(rs->password));
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n");
return -1;
}
for (i = 0; i < hashlen; i++)
sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]);
/* RESET */
gcry_md_reset(hd);
/* HA 2 */
gcry_md_write(hd, method, strlen(method));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, uri, strlen(uri));
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n");
return -1;
}
for (i = 0; i < hashlen; i++)
sprintf(ha2 + (2 * i), hash_fmt, hash_bytes[i]);
/* RESET */
gcry_md_reset(hd);
/* Final value */
gcry_md_write(hd, ha1, 32);
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, rs->nonce, strlen(rs->nonce));
gcry_md_write(hd, ":", 1);
gcry_md_write(hd, ha2, 32);
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n");
return -1;
}
for (i = 0; i < hashlen; i++)
sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]);
gcry_md_close(hd);
/* Build header */
ret = snprintf(auth, sizeof(auth), "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
username, rs->realm, rs->nonce, uri, ha1);
if ((ret < 0) || (ret >= sizeof(auth)))
{
DPRINTF(E_LOG, L_RAOP, "Authorization value header exceeds buffer size\n");
return -1;
}
evrtsp_add_header(req->output_headers, "Authorization", auth);
DPRINTF(E_DBG, L_RAOP, "Authorization header: %s\n", auth);
rs->req_has_auth = 1;
return 0;
}
static int
raop_parse_auth(struct raop_session *rs, struct evrtsp_request *req)
{
const char *param;
char *auth;
char *token;
char *ptr;
if (rs->realm)
{
free(rs->realm);
rs->realm = NULL;
}
if (rs->nonce)
{
free(rs->nonce);
rs->nonce = NULL;
}
param = evrtsp_find_header(req->input_headers, "WWW-Authenticate");
if (!param)
{
DPRINTF(E_LOG, L_RAOP, "WWW-Authenticate header not found\n");
return -1;
}
DPRINTF(E_DBG, L_RAOP, "WWW-Authenticate: %s\n", param);
if (strncmp(param, "Digest ", strlen("Digest ")) != 0)
{
DPRINTF(E_LOG, L_RAOP, "Unsupported authentication method: %s\n", param);
return -1;
}
auth = strdup(param);
if (!auth)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for WWW-Authenticate header copy\n");
return -1;
}
token = strchr(auth, ' ');
token++;
token = strtok_r(token, " =", &ptr);
while (token)
{
if (strcmp(token, "realm") == 0)
{
token = strtok_r(NULL, "=\"", &ptr);
if (!token)
break;
rs->realm = strdup(token);
}
else if (strcmp(token, "nonce") == 0)
{
token = strtok_r(NULL, "=\"", &ptr);
if (!token)
break;
rs->nonce = strdup(token);
}
token = strtok_r(NULL, " =", &ptr);
}
free(auth);
if (!rs->realm || !rs->nonce)
{
DPRINTF(E_LOG, L_RAOP, "Could not find realm/nonce in WWW-Authenticate header\n");
if (rs->realm)
{
free(rs->realm);
rs->realm = NULL;
}
if (rs->nonce)
{
free(rs->nonce);
rs->nonce = NULL;
}
return -1;
}
DPRINTF(E_DBG, L_RAOP, "Found realm: [%s], nonce: [%s]\n", rs->realm, rs->nonce);
return 0;
}
static int
raop_add_headers(struct raop_session *rs, struct evrtsp_request *req, enum evrtsp_cmd_type req_method)
{
char buf[64];
const char *method;
const char *url;
int ret;
method = evrtsp_method(req_method);
DPRINTF(E_DBG, L_RAOP, "Building %s for '%s'\n", method, rs->devname);
snprintf(buf, sizeof(buf), "%d", rs->cseq);
evrtsp_add_header(req->output_headers, "CSeq", buf);
rs->cseq++;
evrtsp_add_header(req->output_headers, "User-Agent", "forked-daapd/" VERSION);
/* Add Authorization header */
url = (req_method == EVRTSP_REQ_OPTIONS) ? "*" : rs->session_url;
ret = raop_add_auth(rs, req, method, url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not add Authorization header\n");
if (ret == -2)
rs->state = RAOP_STATE_PASSWORD;
return -1;
}
snprintf(buf, sizeof(buf), "%" PRIX64, libhash);
evrtsp_add_header(req->output_headers, "Client-Instance", buf);
evrtsp_add_header(req->output_headers, "DACP-ID", buf);
if (rs->session)
evrtsp_add_header(req->output_headers, "Session", rs->session);
/* Content-Length added automatically by evrtsp */
return 0;
}
/* This check should compare the reply CSeq with the request CSeq, but it has
* been removed because RAOP targets like Reflector and AirFoil don't return
* the CSeq according to the rtsp spec, and the CSeq is not really important
* anyway.
*/
static int
raop_check_cseq(struct raop_session *rs, struct evrtsp_request *req)
{
return 0;
}
static int
raop_make_sdp(struct raop_session *rs, struct evrtsp_request *req, char *address, uint32_t session_id)
{
#define SDP_PLD_FMT \
"v=0\r\n" \
"o=iTunes %u 0 IN IP4 %s\r\n" \
"s=iTunes\r\n" \
"c=IN IP4 %s\r\n" \
"t=0 0\r\n" \
"m=audio 0 RTP/AVP 96\r\n" \
"a=rtpmap:96 AppleLossless\r\n" \
"a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n" \
"a=rsaaeskey:%s\r\n" \
"a=aesiv:%s\r\n"
#define SDP_PLD_FMT_NO_ENC \
"v=0\r\n" \
"o=iTunes %u 0 IN IP4 %s\r\n" \
"s=iTunes\r\n" \
"c=IN IP4 %s\r\n" \
"t=0 0\r\n" \
"m=audio 0 RTP/AVP 96\r\n" \
"a=rtpmap:96 AppleLossless\r\n" \
"a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n"
char *p;
int ret;
p = strchr(rs->address, '%');
if (p)
*p = '\0';
/* Add SDP payload - but don't add RSA/AES key/iv if no encryption - important for ATV3 update 6.0 */
if (rs->encrypt)
ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT,
session_id, address, rs->address, AIRTUNES_V2_PACKET_SAMPLES,
raop_aes_key_b64, raop_aes_iv_b64);
else
ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT_NO_ENC,
session_id, address, rs->address, AIRTUNES_V2_PACKET_SAMPLES);
if (p)
*p = '%';
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for SDP payload\n");
return -1;
}
return 0;
#undef SDP_PLD_FMT
#undef SDP_PLD_FMT_NO_ENC
}
/* RAOP/RTSP requests */
/*
* Request queueing HOWTO
*
* Sending:
* - increment rs->reqs_in_flight
* - set evrtsp connection closecb to NULL
*
* Request callback:
* - decrement rs->reqs_in_flight first thing, even if the callback is
* called for error handling (req == NULL or HTTP error code)
* - if rs->reqs_in_flight == 0, setup evrtsp connection closecb
*
* When a request fails, the whole RAOP session is declared failed and
* torn down by calling raop_session_failure(), even if there are requests
* queued on the evrtsp connection. There is no reason to think pending
* requests would work out better than the one that just failed and recovery
* would be tricky to get right.
*
* evrtsp behaviour with queued requests:
* - request callback is called with req == NULL to indicate a connection
* error; if there are several requests queued on the connection, this can
* happen for each request if the connection isn't destroyed
* - the connection is reset, and the closecb is called if the connection was
* previously connected. There is no closecb set when there are requests in
* flight
*/
static int
raop_send_req_teardown(struct raop_session *rs, evrtsp_req_cb cb)
{
struct evrtsp_request *req;
int ret;
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for TEARDOWN\n");
return -1;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_TEARDOWN);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_TEARDOWN, rs->session_url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make TEARDOWN request\n");
return -1;
}
rs->state = RAOP_STATE_CONNECTED;
rs->reqs_in_flight++;
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
return 0;
}
static int
raop_send_req_flush(struct raop_session *rs, uint64_t rtptime, evrtsp_req_cb cb)
{
char buf[64];
struct evrtsp_request *req;
int ret;
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for FLUSH\n");
return -1;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_FLUSH);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
/* Restart sequence: last sequence + 1 */
ret = snprintf(buf, sizeof(buf), "seq=%u;rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rtptime));
if ((ret < 0) || (ret >= sizeof(buf)))
{
DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in FLUSH request\n");
evrtsp_request_free(req);
return -1;
}
evrtsp_add_header(req->output_headers, "RTP-Info", buf);
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_FLUSH, rs->session_url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make FLUSH request\n");
return -1;
}
rs->reqs_in_flight++;
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
return 0;
}
static int
raop_send_req_set_parameter(struct raop_session *rs, struct evbuffer *evbuf, char *ctype, char *rtpinfo, evrtsp_req_cb cb)
{
struct evrtsp_request *req;
int ret;
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for SET_PARAMETER\n");
return -1;
}
ret = evbuffer_add_buffer(req->output_buffer, evbuf);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload\n");
evrtsp_request_free(req);
return -1;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_SET_PARAMETER);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
evrtsp_add_header(req->output_headers, "Content-Type", ctype);
if (rtpinfo)
evrtsp_add_header(req->output_headers, "RTP-Info", rtpinfo);
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_SET_PARAMETER, rs->session_url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make SET_PARAMETER request\n");
return -1;
}
rs->reqs_in_flight++;
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
return 0;
}
static int
raop_send_req_record(struct raop_session *rs, evrtsp_req_cb cb)
{
char buf[64];
struct evrtsp_request *req;
int ret;
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for RECORD\n");
return -1;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_RECORD);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
evrtsp_add_header(req->output_headers, "Range", "npt=0-");
/* Start sequence: next sequence */
ret = snprintf(buf, sizeof(buf), "seq=%u;rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rs->start_rtptime));
if ((ret < 0) || (ret >= sizeof(buf)))
{
DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in RECORD request\n");
evrtsp_request_free(req);
return -1;
}
evrtsp_add_header(req->output_headers, "RTP-Info", buf);
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_RECORD, rs->session_url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make RECORD request\n");
return -1;
}
rs->reqs_in_flight++;
return 0;
}
static int
raop_send_req_setup(struct raop_session *rs, evrtsp_req_cb cb)
{
char hdr[128];
struct evrtsp_request *req;
int ret;
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for SETUP\n");
return -1;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_SETUP);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
/* Request UDP transport, AirTunes v2 streaming */
ret = snprintf(hdr, sizeof(hdr), "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;control_port=%u;timing_port=%u",
rs->control_svc->port, rs->timing_svc->port);
if ((ret < 0) || (ret >= sizeof(hdr)))
{
DPRINTF(E_LOG, L_RAOP, "Transport header exceeds buffer length\n");
evrtsp_request_free(req);
return -1;
}
evrtsp_add_header(req->output_headers, "Transport", hdr);
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_SETUP, rs->session_url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make SETUP request\n");
return -1;
}
rs->reqs_in_flight++;
return 0;
}
static int
raop_send_req_announce(struct raop_session *rs, evrtsp_req_cb cb)
{
uint8_t challenge[16];
char *challenge_b64;
char *ptr;
struct evrtsp_request *req;
char *address;
char *intf;
unsigned short port;
uint32_t session_id;
int ret;
/* Determine local address, needed for SDP and session URL */
evrtsp_connection_get_local_address(rs->ctrl, &address, &port);
if (!address || (port == 0))
{
DPRINTF(E_LOG, L_RAOP, "Could not determine local address\n");
if (address)
free(address);
return -1;
}
intf = strchr(address, '%');
if (intf)
{
*intf = '\0';
intf++;
}
DPRINTF(E_DBG, L_RAOP, "Local address: %s (LL: %s) port %d\n", address, (intf) ? intf : "no", port);
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for ANNOUNCE\n");
free(address);
return -1;
}
/* Session ID and session URL */
gcry_randomize(&session_id, sizeof(session_id), GCRY_STRONG_RANDOM);
ret = snprintf(rs->session_url, sizeof(rs->session_url), "rtsp://%s/%u", address, session_id);
if ((ret < 0) || (ret >= sizeof(rs->session_url)))
{
DPRINTF(E_LOG, L_RAOP, "Session URL length exceeds 127 characters\n");
free(address);
goto cleanup_req;
}
/* SDP payload */
ret = raop_make_sdp(rs, req, address, session_id);
free(address);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not generate SDP payload for ANNOUNCE\n");
goto cleanup_req;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_ANNOUNCE);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
evrtsp_add_header(req->output_headers, "Content-Type", "application/sdp");
/* Challenge - but only if session is encrypted (important for ATV3 after update 6.0) */
if (rs->encrypt)
{
gcry_randomize(challenge, sizeof(challenge), GCRY_STRONG_RANDOM);
challenge_b64 = b64_encode(challenge, sizeof(challenge));
if (!challenge_b64)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't encode challenge\n");
goto cleanup_req;
}
/* Remove base64 padding */
ptr = strchr(challenge_b64, '=');
if (ptr)
*ptr = '\0';
evrtsp_add_header(req->output_headers, "Apple-Challenge", challenge_b64);
free(challenge_b64);
}
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_ANNOUNCE, rs->session_url);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make ANNOUNCE request\n");
return -1;
}
rs->reqs_in_flight++;
return 0;
cleanup_req:
evrtsp_request_free(req);
return -1;
}
static int
raop_send_req_options(struct raop_session *rs, evrtsp_req_cb cb)
{
struct evrtsp_request *req;
int ret;
req = evrtsp_request_new(cb, rs);
if (!req)
{
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for OPTIONS\n");
return -1;
}
ret = raop_add_headers(rs, req, EVRTSP_REQ_OPTIONS);
if (ret < 0)
{
evrtsp_request_free(req);
return -1;
}
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_OPTIONS, "*");
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not make OPTIONS request\n");
return -1;
}
rs->reqs_in_flight++;
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
return 0;
}
/* Maps our internal state to the generic output state and then makes a callback
* to the player to tell that state
*/
static void
raop_status(struct raop_session *rs)
{
output_status_cb status_cb = rs->status_cb;
enum output_device_state state;
switch (rs->state)
{
case RAOP_STATE_PASSWORD:
state = OUTPUT_STATE_PASSWORD;
break;
case RAOP_STATE_FAILED:
state = OUTPUT_STATE_FAILED;
break;
case RAOP_STATE_STOPPED:
state = OUTPUT_STATE_STOPPED;
break;
case RAOP_STATE_STARTUP ... RAOP_STATE_RECORD:
state = OUTPUT_STATE_STARTUP;
break;
case RAOP_STATE_CONNECTED:
state = OUTPUT_STATE_CONNECTED;
break;
case RAOP_STATE_STREAMING:
state = OUTPUT_STATE_STREAMING;
break;
default:
DPRINTF(E_LOG, L_RAOP, "Bug! Unhandled state in cast_status()\n");
state = OUTPUT_STATE_FAILED;
}
rs->status_cb = NULL;
if (status_cb)
status_cb(rs->device, rs->output_session, state);
}
static void
raop_session_free(struct raop_session *rs)
{
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
evrtsp_connection_free(rs->ctrl);
event_free(rs->deferredev);
close(rs->server_fd);
if (rs->realm)
free(rs->realm);
if (rs->nonce)
free(rs->nonce);
if (rs->session)
free(rs->session);
if (rs->address)
free(rs->address);
if (rs->devname)
free(rs->devname);
free(rs->output_session);
free(rs);
rs = NULL;
}
static void
raop_session_cleanup(struct raop_session *rs)
{
struct raop_session *s;
struct raop_v2_packet *pkt;
struct raop_v2_packet *next_pkt;
if (rs == sessions)
sessions = sessions->next;
else
{
for (s = sessions; s && (s->next != rs); s = s->next)
; /* EMPTY */
if (!s)
DPRINTF(E_WARN, L_RAOP, "WARNING: struct raop_session not found in list; BUG!\n");
else
s->next = rs->next;
}
raop_session_free(rs);
/* No more active sessions, free retransmit buffer */
if (!sessions)
{
pkt = pktbuf_head;
while (pkt)
{
next_pkt = pkt->next;
free(pkt);
pkt = next_pkt;
}
pktbuf_head = NULL;
pktbuf_tail = NULL;
pktbuf_size = 0;
}
}
static void
raop_session_failure(struct raop_session *rs)
{
/* Session failed, let our user know */
if (rs->state != RAOP_STATE_PASSWORD)
rs->state = RAOP_STATE_FAILED;
raop_status(rs);
raop_session_cleanup(rs);
}
static void
raop_deferredev_cb(int fd, short what, void *arg)
{
struct raop_session *rs;
rs = (struct raop_session *)arg;
DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device '%s'\n", rs->devname);
raop_session_failure(rs);
}
static void
raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg)
{
struct timeval tv;
struct raop_session *rs;
rs = (struct raop_session *)arg;
DPRINTF(E_LOG, L_RAOP, "Device '%s' closed RTSP connection\n", rs->devname);
rs->state = RAOP_STATE_FAILED;
evutil_timerclear(&tv);
evtimer_add(rs->deferredev, &tv);
}
static struct raop_session *
raop_session_make(struct output_device *rd, int family, output_status_cb cb)
{
struct output_session *os;
struct raop_session *rs;
struct raop_extra *re;
char *address;
char *intf;
unsigned short port;
int ret;
re = rd->extra_device_info;
switch (family)
{
case AF_INET:
/* We always have the v4 services, so no need to check */
if (!rd->v4_address)
return NULL;
address = rd->v4_address;
port = rd->v4_port;
break;
case AF_INET6:
if (!rd->v6_address || (timing_6svc.fd < 0) || (control_6svc.fd < 0))
return NULL;
address = rd->v6_address;
port = rd->v6_port;
break;
default:
return NULL;
}
os = calloc(1, sizeof(struct output_session));
if (!os)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory (os)\n");
return NULL;
}
rs = calloc(1, sizeof(struct raop_session));
if (!rs)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory (rs)\n");
free(os);
return NULL;
}
os->session = rs;
os->type = rd->type;
rs->output_session = os;
rs->state = RAOP_STATE_STOPPED;
rs->reqs_in_flight = 0;
rs->cseq = 1;
rs->device = rd;
rs->status_cb = cb;
rs->server_fd = -1;
rs->password = rd->password;
rs->wants_metadata = re->wants_metadata;
switch (re->devtype)
{
case RAOP_DEV_APEX1_80211G:
rs->encrypt = 1;
rs->auth_quirk_itunes = 1;
rs->keep_alive = 0;
break;
case RAOP_DEV_APEX2_80211N:
rs->encrypt = 1;
rs->auth_quirk_itunes = 0;
rs->keep_alive = 0;
break;
case RAOP_DEV_APEX3_80211N:
rs->encrypt = 0;
rs->auth_quirk_itunes = 0;
rs->keep_alive = 0;
break;
case RAOP_DEV_APPLETV:
rs->encrypt = 0;
rs->auth_quirk_itunes = 0;
rs->keep_alive = 0;
break;
case RAOP_DEV_APPLETV4:
rs->encrypt = 0;
rs->auth_quirk_itunes = 0;
rs->keep_alive = 1;
break;
case RAOP_DEV_OTHER:
rs->encrypt = re->encrypt;
rs->auth_quirk_itunes = 0;
rs->keep_alive = 0;
break;
}
rs->deferredev = evtimer_new(evbase_player, raop_deferredev_cb, rs);
if (!rs->deferredev)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for deferred error handling!\n");
goto out_free_rs;
}
rs->ctrl = evrtsp_connection_new(address, port);
if (!rs->ctrl)
{
DPRINTF(E_LOG, L_RAOP, "Could not create control connection to '%s' (%s)\n", rd->name, address);
goto out_free_event;
}
evrtsp_connection_set_base(rs->ctrl, evbase_player);
rs->sa.ss.ss_family = family;
switch (family)
{
case AF_INET:
rs->timing_svc = &timing_4svc;
rs->control_svc = &control_4svc;
ret = inet_pton(AF_INET, address, &rs->sa.sin.sin_addr);
break;
case AF_INET6:
rs->timing_svc = &timing_6svc;
rs->control_svc = &control_6svc;
intf = strchr(address, '%');
if (intf)
*intf = '\0';
ret = inet_pton(AF_INET6, address, &rs->sa.sin6.sin6_addr);
if (intf)
{
*intf = '%';
intf++;
rs->sa.sin6.sin6_scope_id = if_nametoindex(intf);
if (rs->sa.sin6.sin6_scope_id == 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not find interface %s\n", intf);
ret = -1;
break;
}
}
break;
default:
ret = -1;
break;
}
if (ret <= 0)
{
DPRINTF(E_LOG, L_RAOP, "Device '%s' has invalid address (%s) for %s\n", rd->name, address, (family == AF_INET) ? "ipv4" : "ipv6");
goto out_free_evcon;
}
rs->devname = strdup(rd->name);
rs->address = strdup(address);
rs->volume = rd->volume;
rs->next = sessions;
sessions = rs;
return rs;
out_free_evcon:
evrtsp_connection_free(rs->ctrl);
out_free_event:
event_free(rs->deferredev);
out_free_rs:
free(rs);
return NULL;
}
static void
raop_session_failure_cb(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
rs = (struct raop_session *)arg;
raop_session_failure(rs);
}
/* Metadata handling */
static void
raop_cb_metadata(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto error;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for metadata/artwork/progress: %d %s\n", req->response_code, req->response_code_line);
goto error;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto error;
/* No status_cb call, user doesn't want/need to know about the status
* of metadata requests unless they cause the session to fail.
*/
if (!rs->reqs_in_flight)
evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs);
return;
error:
raop_session_failure(rs);
}
static int
raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint64_t offset, uint32_t delay)
{
uint32_t display;
int ret;
/* Here's the deal with progress values:
* - first value, called display, is always start minus a delay
* -> delay x1 if streaming is starting for this device (joining or not)
* -> delay x2 if stream is switching to a new song
* - second value, called start, is the RTP time of the first sample for this
* song for this device
* -> start of song
* -> start of song + offset if device is joining in the middle of a song,
* or getting out of a pause or seeking
* - third value, called end, is the RTP time of the last sample for this song
*/
display = RAOP_RTPTIME(rmd->start - delay);
ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, RAOP_RTPTIME(rmd->start + offset), RAOP_RTPTIME(rmd->end));
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not build progress string for sending\n");
return -1;
}
ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata);
if (ret < 0)
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n");
return ret;
}
static int
raop_metadata_send_artwork(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, char *rtptime)
{
char *ctype;
uint8_t *buf;
size_t len;
int ret;
switch (rmd->artwork_fmt)
{
case ART_FMT_PNG:
ctype = "image/png";
break;
case ART_FMT_JPEG:
ctype = "image/jpeg";
break;
default:
DPRINTF(E_LOG, L_RAOP, "Unsupported artwork format %d\n", rmd->artwork_fmt);
return -1;
}
buf = evbuffer_pullup(rmd->artwork, -1);
len = evbuffer_get_length(rmd->artwork);
ret = evbuffer_add(evbuf, buf, len);
if (ret != 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not copy artwork for sending\n");
return -1;
}
ret = raop_send_req_set_parameter(rs, evbuf, ctype, rtptime, raop_cb_metadata);
if (ret < 0)
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n");
return ret;
}
static int
raop_metadata_send_metadata(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, char *rtptime)
{
uint8_t *buf;
size_t len;
int ret;
buf = evbuffer_pullup(rmd->metadata, -1);
len = evbuffer_get_length(rmd->metadata);
ret = evbuffer_add(evbuf, buf, len);
if (ret != 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not copy metadata for sending\n");
return -1;
}
ret = raop_send_req_set_parameter(rs, evbuf, "application/x-dmap-tagged", rtptime, raop_cb_metadata);
if (ret < 0)
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n");
return ret;
}
static int
raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, uint64_t offset, uint32_t delay)
{
char rtptime[32];
struct evbuffer *evbuf;
int ret;
evbuf = evbuffer_new();
if (!evbuf)
{
DPRINTF(E_LOG, L_RAOP, "Could not allocate temp evbuffer for metadata processing\n");
return -1;
}
ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", RAOP_RTPTIME(rmd->start));
if ((ret < 0) || (ret >= sizeof(rtptime)))
{
DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer while sending metadata\n");
ret = -1;
goto out;
}
ret = raop_metadata_send_metadata(rs, evbuf, rmd, rtptime);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not send metadata to '%s'\n", rs->devname);
ret = -1;
goto out;
}
if (!rmd->artwork)
goto skip_artwork;
ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not send artwork to '%s'\n", rs->devname);
ret = -1;
goto out;
}
skip_artwork:
ret = raop_metadata_send_progress(rs, evbuf, rmd, offset, delay);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not send progress to '%s'\n", rs->devname);
ret = -1;
goto out;
}
out:
evbuffer_free(evbuf);
return ret;
}
static void
raop_metadata_startup_send(struct raop_session *rs)
{
struct raop_metadata *rmd;
uint64_t offset;
int sent;
int ret;
if (!rs->wants_metadata)
return;
sent = 0;
for (rmd = metadata_head; rmd; rmd = rmd->next)
{
/* Current song */
if ((rs->start_rtptime >= rmd->start) && (rs->start_rtptime < rmd->end))
{
offset = rs->start_rtptime - rmd->start;
ret = raop_metadata_send_internal(rs, rmd, offset, RAOP_MD_DELAY_STARTUP);
if (ret < 0)
{
raop_session_failure(rs);
return;
}
sent = 1;
}
/* Next song(s) */
else if (sent && (rs->start_rtptime < rmd->start))
{
ret = raop_metadata_send_internal(rs, rmd, 0, RAOP_MD_DELAY_SWITCH);
if (ret < 0)
{
raop_session_failure(rs);
return;
}
}
}
}
static void
raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startup)
{
struct raop_metadata *rmd;
struct raop_session *rs;
struct raop_session *next;
uint32_t delay;
int ret;
rmd = metadata;
rmd->start += rtptime;
rmd->end += rtptime;
/* Add the rmd to the metadata list */
if (metadata_tail)
metadata_tail->next = rmd;
else
{
metadata_head = rmd;
metadata_tail = rmd;
}
for (rs = sessions; rs; rs = next)
{
next = rs->next;
if (!(rs->state & RAOP_STATE_F_CONNECTED))
continue;
if (!rs->wants_metadata)
continue;
delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH;
ret = raop_metadata_send_internal(rs, rmd, offset, delay);
if (ret < 0)
{
raop_session_failure(rs);
continue;
}
}
}
/* Volume handling */
static float
raop_volume_convert(int volume, char *name)
{
float raop_volume;
cfg_t *airplay;
int max_volume;
max_volume = RAOP_CONFIG_MAX_VOLUME;
airplay = cfg_gettsec(cfg, "airplay", name);
if (airplay)
max_volume = cfg_getint(airplay, "max_volume");
if ((max_volume < 1) || (max_volume > RAOP_CONFIG_MAX_VOLUME))
{
DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device %s, using default instead\n", max_volume, name);
max_volume = RAOP_CONFIG_MAX_VOLUME;
}
/* RAOP volume
* -144.0 is off
* 0 - 100 maps to -30.0 - 0
*/
if (volume > 0 && volume <= 100)
raop_volume = -30.0 + ((float)max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME);
else
raop_volume = -144.0;
return raop_volume;
}
static int
raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb)
{
struct evbuffer *evbuf;
float raop_volume;
int ret;
evbuf = evbuffer_new();
if (!evbuf)
{
DPRINTF(E_LOG, L_RAOP, "Could not allocate evbuffer for volume payload\n");
return -1;
}
raop_volume = raop_volume_convert(volume, rs->devname);
/* Don't let locales get in the way here */
/* We use -%d and -(int)raop_volume so -0.3 won't become 0.3 */
ret = evbuffer_add_printf(evbuf, "volume: -%d.%06d\r\n", -(int)raop_volume, -(int)(1000000.0 * (raop_volume - (int)raop_volume)));
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload (volume)\n");
evbuffer_free(evbuf);
return -1;
}
ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, cb);
if (ret < 0)
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for volume\n");
evbuffer_free(evbuf);
return ret;
}
static void
raop_cb_set_volume(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto error;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for stream volume: %d %s\n", req->response_code, req->response_code_line);
goto error;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto error;
/* Let our user know */
raop_status(rs);
if (!rs->reqs_in_flight)
evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs);
return;
error:
raop_session_failure(rs);
}
/* Volume in [0 - 100] */
static int
raop_set_volume_one(struct output_device *rd, output_status_cb cb)
{
struct raop_session *rs;
int ret;
if (!rd->session || !rd->session->session)
return 0;
rs = rd->session->session;
if (!(rs->state & RAOP_STATE_F_CONNECTED))
return 0;
ret = raop_set_volume_internal(rs, rd->volume, raop_cb_set_volume);
if (ret < 0)
{
raop_session_failure(rs);
return 0;
}
rs->status_cb = cb;
return 1;
}
static void
raop_cb_flush(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto error;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "FLUSH request failed: %d %s\n", req->response_code, req->response_code_line);
goto error;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto error;
rs->state = RAOP_STATE_CONNECTED;
/* Let our user know */
raop_status(rs);
if (!rs->reqs_in_flight)
evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs);
return;
error:
raop_session_failure(rs);
}
static void
raop_cb_keep_alive(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs = arg;
rs->reqs_in_flight--;
if (!req)
goto error;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for keep alive: %d %s\n", req->response_code, req->response_code_line);
goto error;
}
if (!rs->reqs_in_flight)
evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs);
return;
error:
raop_session_failure(rs);
}
// Forward
static void
raop_device_stop(struct output_session *session);
static void
raop_flush_timer_cb(int fd, short what, void *arg)
{
struct raop_session *rs;
DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP sessions\n");
for (rs = sessions; rs; rs = rs->next)
{
if (!(rs->state & RAOP_STATE_F_CONNECTED))
continue;
raop_device_stop(rs->output_session);
}
}
static void
raop_keep_alive_timer_cb(int fd, short what, void *arg)
{
struct raop_session *rs;
for (rs = sessions; rs; rs = rs->next)
{
if (!rs->keep_alive)
continue;
if (!(rs->state & RAOP_STATE_F_CONNECTED))
continue;
raop_set_volume_internal(rs, rs->volume, raop_cb_keep_alive);
}
evtimer_add(keep_alive_timer, &keep_alive_tv);
}
static int
raop_flush(output_status_cb cb, uint64_t rtptime)
{
struct timeval tv;
struct raop_session *rs;
struct raop_session *next;
int pending;
int ret;
pending = 0;
for (rs = sessions; rs; rs = next)
{
next = rs->next;
if (rs->state != RAOP_STATE_STREAMING)
continue;
ret = raop_send_req_flush(rs, rtptime, raop_cb_flush);
if (ret < 0)
{
raop_session_failure(rs);
continue;
}
rs->status_cb = cb;
pending++;
}
if (pending > 0)
{
evutil_timerclear(&tv);
tv.tv_sec = 10;
evtimer_add(flush_timer, &tv);
}
return pending;
}
/* AirTunes v2 time synchronization */
static void
raop_v2_timing_cb(int fd, short what, void *arg)
{
union sockaddr_all sa;
uint8_t req[32];
uint8_t res[32];
struct ntp_stamp recv_stamp;
struct ntp_stamp xmit_stamp;
struct raop_service *svc;
int len;
int ret;
svc = (struct raop_service *)arg;
ret = raop_v2_timing_get_clock_ntp(&recv_stamp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't get receive timestamp\n");
goto readd;
}
len = sizeof(sa.ss);
ret = recvfrom(svc->fd, req, sizeof(req), 0, &sa.sa, (socklen_t *)&len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Error reading timing request: %s\n", strerror(errno));
goto readd;
}
if (ret != 32)
{
DPRINTF(E_DBG, L_RAOP, "Got timing request with size %d\n", ret);
goto readd;
}
if ((req[0] != 0x80) || (req[1] != 0xd2))
{
DPRINTF(E_LOG, L_RAOP, "Packet header doesn't match timing request\n");
goto readd;
}
memset(res, 0, sizeof(res));
/* Header */
res[0] = 0x80;
res[1] = 0xd3;
res[2] = req[2];
/* Copy client timestamp */
memcpy(res + 8, req + 24, 8);
/* Receive timestamp */
recv_stamp.sec = htobe32(recv_stamp.sec);
recv_stamp.frac = htobe32(recv_stamp.frac);
memcpy(res + 16, &recv_stamp.sec, 4);
memcpy(res + 20, &recv_stamp.frac, 4);
/* Transmit timestamp */
ret = raop_v2_timing_get_clock_ntp(&xmit_stamp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't get transmit timestamp, falling back to receive timestamp\n");
/* Still better than failing altogether
* recv/xmit are close enough that it shouldn't matter much
*/
memcpy(res + 24, &recv_stamp.sec, 4);
memcpy(res + 28, &recv_stamp.frac, 4);
}
else
{
xmit_stamp.sec = htobe32(xmit_stamp.sec);
xmit_stamp.frac = htobe32(xmit_stamp.frac);
memcpy(res + 24, &xmit_stamp.sec, 4);
memcpy(res + 28, &xmit_stamp.frac, 4);
}
ret = sendto(svc->fd, res, sizeof(res), 0, &sa.sa, len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not send timing reply: %s\n", strerror(errno));
goto readd;
}
readd:
ret = event_add(svc->ev, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't re-add event for timing requests\n");
return;
}
}
static int
raop_v2_timing_start_one(struct raop_service *svc, int family)
{
union sockaddr_all sa;
int on;
int len;
int ret;
#ifdef SOCK_CLOEXEC
svc->fd = socket(family, SOCK_DGRAM | SOCK_CLOEXEC, 0);
#else
svc->fd = socket(family, SOCK_DGRAM, 0);
#endif
if (svc->fd < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't make timing socket: %s\n", strerror(errno));
return -1;
}
if (family == AF_INET6)
{
on = 1;
ret = setsockopt(svc->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on));
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not set IPV6_V6ONLY on timing socket: %s\n", strerror(errno));
goto out_fail;
}
}
memset(&sa, 0, sizeof(union sockaddr_all));
sa.ss.ss_family = family;
switch (family)
{
case AF_INET:
sa.sin.sin_addr.s_addr = INADDR_ANY;
sa.sin.sin_port = 0;
len = sizeof(sa.sin);
break;
case AF_INET6:
sa.sin6.sin6_addr = in6addr_any;
sa.sin6.sin6_port = 0;
len = sizeof(sa.sin6);
break;
}
ret = bind(svc->fd, &sa.sa, len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't bind timing socket: %s\n", strerror(errno));
goto out_fail;
}
len = sizeof(sa.ss);
ret = getsockname(svc->fd, &sa.sa, (socklen_t *)&len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't get timing socket name: %s\n", strerror(errno));
goto out_fail;
}
switch (family)
{
case AF_INET:
svc->port = ntohs(sa.sin.sin_port);
DPRINTF(E_DBG, L_RAOP, "Timing IPv4 port: %d\n", svc->port);
break;
case AF_INET6:
svc->port = ntohs(sa.sin6.sin6_port);
DPRINTF(E_DBG, L_RAOP, "Timing IPv6 port: %d\n", svc->port);
break;
}
svc->ev = event_new(evbase_player, svc->fd, EV_READ, raop_v2_timing_cb, svc);
if (!svc->ev)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for raop_service event\n");
goto out_fail;
}
event_add(svc->ev, NULL);
return 0;
out_fail:
close(svc->fd);
svc->fd = -1;
svc->port = 0;
return -1;
}
static void
raop_v2_timing_stop(void)
{
if (timing_4svc.ev)
event_free(timing_4svc.ev);
if (timing_6svc.ev)
event_free(timing_6svc.ev);
close(timing_4svc.fd);
timing_4svc.fd = -1;
timing_4svc.port = 0;
close(timing_6svc.fd);
timing_6svc.fd = -1;
timing_6svc.port = 0;
}
static int
raop_v2_timing_start(int v6enabled)
{
int ret;
if (v6enabled)
{
ret = raop_v2_timing_start_one(&timing_6svc, AF_INET6);
if (ret < 0)
DPRINTF(E_WARN, L_RAOP, "Could not start timing service on IPv6\n");
}
ret = raop_v2_timing_start_one(&timing_4svc, AF_INET);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not start timing service on IPv4\n");
raop_v2_timing_stop();
return -1;
}
return 0;
}
/* AirTunes v2 playback synchronization */
static void
raop_v2_control_send_sync(uint64_t next_pkt, struct timespec *init)
{
uint8_t msg[20];
struct timespec ts;
struct ntp_stamp cur_stamp;
struct raop_session *rs;
uint64_t cur_pos;
uint32_t cur_pos32;
uint32_t next_pkt32;
int len;
int ret;
memset(msg, 0, sizeof(msg));
msg[0] = (sync_counter == 0) ? 0x90 : 0x80;
msg[1] = 0xd4;
msg[3] = 0x07;
next_pkt32 = htobe32(RAOP_RTPTIME(next_pkt));
memcpy(msg + 16, &next_pkt32, 4);
if (!init)
{
ret = player_get_current_pos(&cur_pos, &ts, 1);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not get current playback position and clock\n");
return;
}
timespec_to_ntp(&ts, &cur_stamp);
}
else
{
cur_pos = next_pkt - 88200;
timespec_to_ntp(init, &cur_stamp);
}
cur_pos32 = htobe32(RAOP_RTPTIME(cur_pos));
cur_stamp.sec = htobe32(cur_stamp.sec);
cur_stamp.frac = htobe32(cur_stamp.frac);
memcpy(msg + 4, &cur_pos32, 4);
memcpy(msg + 8, &cur_stamp.sec, 4);
memcpy(msg + 12, &cur_stamp.frac, 4);
for (rs = sessions; rs; rs = rs->next)
{
if (rs->state != RAOP_STATE_STREAMING)
continue;
switch (rs->sa.ss.ss_family)
{
case AF_INET:
rs->sa.sin.sin_port = htons(rs->control_port);
len = sizeof(rs->sa.sin);
break;
case AF_INET6:
rs->sa.sin6.sin6_port = htons(rs->control_port);
len = sizeof(rs->sa.sin6);
break;
default:
DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family);
continue;
}
ret = sendto(rs->control_svc->fd, msg, sizeof(msg), 0, &rs->sa.sa, len);
if (ret < 0)
DPRINTF(E_LOG, L_RAOP, "Could not send playback sync to device '%s': %s\n", rs->devname, strerror(errno));
}
}
/* Forward */
static void
raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len);
static void
raop_v2_control_cb(int fd, short what, void *arg)
{
char address[INET6_ADDRSTRLEN];
union sockaddr_all sa;
uint8_t req[8];
struct raop_session *rs;
struct raop_service *svc;
uint16_t seq_start;
uint16_t seq_len;
int len;
int ret;
svc = (struct raop_service *)arg;
len = sizeof(sa.ss);
ret = recvfrom(svc->fd, req, sizeof(req), 0, &sa.sa, (socklen_t *)&len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Error reading control request: %s\n", strerror(errno));
goto readd;
}
if (ret != 8)
{
DPRINTF(E_DBG, L_RAOP, "Got control request with size %d\n", ret);
goto readd;
}
switch (sa.ss.ss_family)
{
case AF_INET:
if (svc != &control_4svc)
goto readd;
for (rs = sessions; rs; rs = rs->next)
{
if ((rs->sa.ss.ss_family == AF_INET)
&& (sa.sin.sin_addr.s_addr == rs->sa.sin.sin_addr.s_addr))
break;
}
if (!rs)
ret = (inet_ntop(AF_INET, &sa.sin.sin_addr.s_addr, address, sizeof(address)) != NULL);
break;
case AF_INET6:
if (svc != &control_6svc)
goto readd;
for (rs = sessions; rs; rs = rs->next)
{
if ((rs->sa.ss.ss_family == AF_INET6)
&& IN6_ARE_ADDR_EQUAL(&sa.sin6.sin6_addr, &rs->sa.sin6.sin6_addr))
break;
}
if (!rs)
ret = (inet_ntop(AF_INET6, &sa.sin6.sin6_addr.s6_addr, address, sizeof(address)) != NULL);
break;
default:
DPRINTF(E_LOG, L_RAOP, "Control svc: Unknown address family %d\n", sa.ss.ss_family);
goto readd;
}
if (!rs)
{
if (!ret)
DPRINTF(E_LOG, L_RAOP, "Control request from [error: %s]; not a RAOP client\n", strerror(errno));
else
DPRINTF(E_LOG, L_RAOP, "Control request from %s; not a RAOP client\n", address);
goto readd;
}
if ((req[0] != 0x80) || (req[1] != 0xd5))
{
DPRINTF(E_LOG, L_RAOP, "Packet header doesn't match retransmit request\n");
goto readd;
}
memcpy(&seq_start, req + 4, 2);
memcpy(&seq_len, req + 6, 2);
seq_start = be16toh(seq_start);
seq_len = be16toh(seq_len);
DPRINTF(E_DBG, L_RAOP, "Got retransmit request, seq_start %u len %u\n", seq_start, seq_len);
raop_v2_resend_range(rs, seq_start, seq_len);
readd:
ret = event_add(svc->ev, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't re-add event for control requests\n");
return;
}
}
static int
raop_v2_control_start_one(struct raop_service *svc, int family)
{
union sockaddr_all sa;
int on;
int len;
int ret;
#ifdef SOCK_CLOEXEC
svc->fd = socket(family, SOCK_DGRAM | SOCK_CLOEXEC, 0);
#else
svc->fd = socket(family, SOCK_DGRAM, 0);
#endif
if (svc->fd < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't make control socket: %s\n", strerror(errno));
return -1;
}
if (family == AF_INET6)
{
on = 1;
ret = setsockopt(svc->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on));
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not set IPV6_V6ONLY on control socket: %s\n", strerror(errno));
goto out_fail;
}
}
memset(&sa, 0, sizeof(union sockaddr_all));
sa.ss.ss_family = family;
switch (family)
{
case AF_INET:
sa.sin.sin_addr.s_addr = INADDR_ANY;
sa.sin.sin_port = 0;
len = sizeof(sa.sin);
break;
case AF_INET6:
sa.sin6.sin6_addr = in6addr_any;
sa.sin6.sin6_port = 0;
len = sizeof(sa.sin6);
break;
}
ret = bind(svc->fd, &sa.sa, len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't bind control socket: %s\n", strerror(errno));
goto out_fail;
}
len = sizeof(sa.ss);
ret = getsockname(svc->fd, &sa.sa, (socklen_t *)&len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't get control socket name: %s\n", strerror(errno));
goto out_fail;
}
switch (family)
{
case AF_INET:
svc->port = ntohs(sa.sin.sin_port);
DPRINTF(E_DBG, L_RAOP, "Control IPv4 port: %d\n", svc->port);
break;
case AF_INET6:
svc->port = ntohs(sa.sin6.sin6_port);
DPRINTF(E_DBG, L_RAOP, "Control IPv6 port: %d\n", svc->port);
break;
}
svc->ev = event_new(evbase_player, svc->fd, EV_READ, raop_v2_control_cb, svc);
if (!svc->ev)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for control event\n");
goto out_fail;
}
event_add(svc->ev, NULL);
return 0;
out_fail:
close(svc->fd);
svc->fd = -1;
svc->port = 0;
return -1;
}
static void
raop_v2_control_stop(void)
{
if (control_4svc.ev)
event_free(control_4svc.ev);
if (control_6svc.ev)
event_free(control_6svc.ev);
close(control_4svc.fd);
control_4svc.fd = -1;
control_4svc.port = 0;
close(control_6svc.fd);
control_6svc.fd = -1;
control_6svc.port = 0;
}
static int
raop_v2_control_start(int v6enabled)
{
int ret;
if (v6enabled)
{
ret = raop_v2_control_start_one(&control_6svc, AF_INET6);
if (ret < 0)
DPRINTF(E_WARN, L_RAOP, "Could not start control service on IPv6\n");
}
ret = raop_v2_control_start_one(&control_4svc, AF_INET);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not start control service on IPv4\n");
raop_v2_control_stop();
return -1;
}
return 0;
}
/* AirTunes v2 streaming */
static struct raop_v2_packet *
raop_v2_new_packet(void)
{
struct raop_v2_packet *pkt;
if (pktbuf_size >= RETRANSMIT_BUFFER_SIZE)
{
pktbuf_size--;
pkt = pktbuf_tail;
pktbuf_tail = pktbuf_tail->prev;
pktbuf_tail->next = NULL;
}
else
{
pkt = (struct raop_v2_packet *)malloc(sizeof(struct raop_v2_packet));
if (!pkt)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP packet\n");
return NULL;
}
}
return pkt;
}
static struct raop_v2_packet *
raop_v2_make_packet(uint8_t *rawbuf, uint64_t rtptime)
{
char ebuf[64];
struct raop_v2_packet *pkt;
gpg_error_t gc_err;
uint32_t rtptime32;
uint16_t seq;
pkt = raop_v2_new_packet();
if (!pkt)
return NULL;
memset(pkt, 0, sizeof(struct raop_v2_packet));
alac_encode(rawbuf, pkt->clear + AIRTUNES_V2_HDR_LEN, STOB(AIRTUNES_V2_PACKET_SAMPLES));
stream_seq++;
pkt->seqnum = stream_seq;
seq = htobe16(pkt->seqnum);
rtptime32 = htobe32(RAOP_RTPTIME(rtptime));
pkt->clear[0] = 0x80;
pkt->clear[1] = (sync_counter == 0) ? 0xe0 : 0x60;
memcpy(pkt->clear + 2, &seq, 2);
memcpy(pkt->clear + 4, &rtptime32, 4);
/* RTP SSRC ID
* Note: should htobe32() that value, but it's just a
* random/unique ID so it's no big deal
*/
memcpy(pkt->clear + 8, &ssrc_id, 4);
/* Copy AirTunes v2 header to encrypted packet */
memcpy(pkt->encrypted, pkt->clear, AIRTUNES_V2_HDR_LEN);
/* Copy the tail of the audio packet that is left unencrypted */
memcpy(pkt->encrypted + AIRTUNES_V2_PKT_TAIL_OFF,
pkt->clear + AIRTUNES_V2_PKT_TAIL_OFF,
AIRTUNES_V2_PKT_TAIL_LEN);
/* Reset cipher */
gc_err = gcry_cipher_reset(raop_aes_ctx);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not reset AES cipher: %s\n", ebuf);
free(pkt);
return NULL;
}
/* Set IV */
gc_err = gcry_cipher_setiv(raop_aes_ctx, raop_aes_iv, sizeof(raop_aes_iv));
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not set AES IV: %s\n", ebuf);
free(pkt);
return NULL;
}
/* Encrypt in blocks of 16 bytes */
gc_err = gcry_cipher_encrypt(raop_aes_ctx,
pkt->encrypted + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16,
pkt->clear + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not encrypt payload: %s\n", ebuf);
free(pkt);
return NULL;
}
pkt->prev = NULL;
pkt->next = pktbuf_head;
if (pktbuf_head)
pktbuf_head->prev = pkt;
if (!pktbuf_tail)
pktbuf_tail = pkt;
pktbuf_head = pkt;
pktbuf_size++;
return pkt;
}
static int
raop_v2_send_packet(struct raop_session *rs, struct raop_v2_packet *pkt)
{
uint8_t *data;
int ret;
if (!rs)
return -1;
data = (rs->encrypt) ? pkt->encrypted : pkt->clear;
ret = send(rs->server_fd, data, AIRTUNES_V2_PKT_LEN, 0);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno));
raop_session_failure(rs);
return -1;
}
else if (ret != AIRTUNES_V2_PKT_LEN)
{
DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for '%s'\n", ret, rs->devname);
return -1;
}
return 0;
}
// Forward
static void
raop_playback_stop(void);
static void
raop_v2_write(uint8_t *buf, uint64_t rtptime)
{
struct raop_v2_packet *pkt;
struct raop_session *rs;
struct raop_session *next;
pkt = raop_v2_make_packet(buf, rtptime);
if (!pkt)
{
raop_playback_stop();
return;
}
if (sync_counter == 126)
{
raop_v2_control_send_sync(rtptime, NULL);
sync_counter = 1;
}
else
sync_counter++;
for (rs = sessions; rs; rs = next)
{
// raop_v2_send_packet may free rs on failure, so save rs->next now
next = rs->next;
if (rs->state != RAOP_STATE_STREAMING)
continue;
raop_v2_send_packet(rs, pkt);
}
return;
}
static void
raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len)
{
struct raop_v2_packet *pktbuf;
int ret;
uint16_t distance;
/* Check that seqnum is in the retransmit buffer */
if ((seqnum > pktbuf_head->seqnum) || (seqnum < pktbuf_tail->seqnum))
{
DPRINTF(E_WARN, L_RAOP, "Device '%s' asking for seqnum %u; not in buffer (h %u t %u)\n", rs->devname, seqnum, pktbuf_head->seqnum, pktbuf_tail->seqnum);
return;
}
if (seqnum > pktbuf_head->seqnum)
{
distance = seqnum - pktbuf_tail->seqnum;
if (distance > (RETRANSMIT_BUFFER_SIZE / 2))
pktbuf = pktbuf_head;
else
pktbuf = pktbuf_tail;
}
else
{
distance = pktbuf_head->seqnum - seqnum;
if (distance > (RETRANSMIT_BUFFER_SIZE / 2))
pktbuf = pktbuf_tail;
else
pktbuf = pktbuf_head;
}
if (pktbuf == pktbuf_head)
{
while (pktbuf && seqnum != pktbuf->seqnum)
pktbuf = pktbuf->next;
}
else
{
while (pktbuf && seqnum != pktbuf->seqnum)
pktbuf = pktbuf->prev;
}
while (len && pktbuf)
{
ret = raop_v2_send_packet(rs, pktbuf);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Error retransmit packet, aborting retransmission\n");
return;
}
pktbuf = pktbuf->prev;
len--;
}
if (len != 0)
DPRINTF(E_LOG, L_RAOP, "WARNING: len non-zero at end of retransmission\n");
}
static int
raop_v2_stream_open(struct raop_session *rs)
{
int len;
int ret;
#ifdef SOCK_CLOEXEC
rs->server_fd = socket(rs->sa.ss.ss_family, SOCK_DGRAM | SOCK_CLOEXEC, 0);
#else
rs->server_fd = socket(rs->sa.ss.ss_family, SOCK_DGRAM, 0);
#endif
if (rs->server_fd < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not create socket for streaming: %s\n", strerror(errno));
return -1;
}
switch (rs->sa.ss.ss_family)
{
case AF_INET:
rs->sa.sin.sin_port = htons(rs->server_port);
len = sizeof(rs->sa.sin);
break;
case AF_INET6:
rs->sa.sin6.sin6_port = htons(rs->server_port);
len = sizeof(rs->sa.sin6);
break;
default:
DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family);
goto out_fail;
}
ret = connect(rs->server_fd, &rs->sa.sa, len);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "connect() to [%s]:%u failed: %s\n", rs->address, rs->server_port, strerror(errno));
goto out_fail;
}
/* Include the device into the set of active devices if
* playback is in progress.
*/
if (sync_counter != 0)
rs->state = RAOP_STATE_STREAMING;
else
rs->state = RAOP_STATE_CONNECTED;
return 0;
out_fail:
close(rs->server_fd);
rs->server_fd = -1;
return -1;
}
/* Session startup */
static void
raop_startup_cancel(struct raop_session *rs)
{
/* Try being nice to our peer */
if (rs->session)
raop_send_req_teardown(rs, raop_session_failure_cb);
else
raop_session_failure(rs);
}
static void
raop_cb_startup_volume(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto cleanup;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for startup volume: %d %s\n", req->response_code, req->response_code_line);
goto cleanup;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto cleanup;
raop_metadata_startup_send(rs);
ret = raop_v2_stream_open(rs);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not open streaming socket\n");
goto cleanup;
}
/* Session startup and setup is done, tell our user */
raop_status(rs);
if (!rs->reqs_in_flight)
evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs);
return;
cleanup:
raop_startup_cancel(rs);
}
static void
raop_cb_startup_record(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
const char *param;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto cleanup;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "RECORD request failed in session startup: %d %s\n", req->response_code, req->response_code_line);
goto cleanup;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto cleanup;
/* Audio latency */
param = evrtsp_find_header(req->input_headers, "Audio-Latency");
if (!param)
DPRINTF(E_INFO, L_RAOP, "RECORD reply from '%s' did not have an Audio-Latency header\n", rs->devname);
else
DPRINTF(E_DBG, L_RAOP, "RAOP audio latency is %s\n", param);
rs->state = RAOP_STATE_RECORD;
/* Set initial volume */
raop_set_volume_internal(rs, rs->volume, raop_cb_startup_volume);
return;
cleanup:
raop_startup_cancel(rs);
}
static void
raop_cb_startup_setup(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
const char *param;
char *transport;
char *token;
char *ptr;
int tmp;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto cleanup;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "SETUP request failed in session startup: %d %s\n", req->response_code, req->response_code_line);
goto cleanup;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto cleanup;
/* Server-side session ID */
param = evrtsp_find_header(req->input_headers, "Session");
if (!param)
{
DPRINTF(E_LOG, L_RAOP, "Missing Session header in SETUP reply\n");
goto cleanup;
}
rs->session = strdup(param);
/* Check transport and get remote streaming port */
param = evrtsp_find_header(req->input_headers, "Transport");
if (!param)
{
DPRINTF(E_LOG, L_RAOP, "Missing Transport header in SETUP reply\n");
goto cleanup;
}
/* Check transport is really UDP, AirTunes v2 streaming */
if (strncmp(param, "RTP/AVP/UDP;", strlen("RTP/AVP/UDP;")) != 0)
{
DPRINTF(E_LOG, L_RAOP, "ApEx replied with unsupported Transport: %s\n", param);
goto cleanup;
}
transport = strdup(param);
if (!transport)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for Transport header copy\n");
goto cleanup;
}
token = strchr(transport, ';');
token++;
token = strtok_r(token, ";=", &ptr);
while (token)
{
DPRINTF(E_DBG, L_RAOP, "token: %s\n", token);
if (strcmp(token, "server_port") == 0)
{
token = strtok_r(NULL, ";=", &ptr);
if (!token)
break;
ret = safe_atoi32(token, &tmp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not read server_port\n");
break;
}
rs->server_port = tmp;
}
else if (strcmp(token, "control_port") == 0)
{
token = strtok_r(NULL, ";=", &ptr);
if (!token)
break;
ret = safe_atoi32(token, &tmp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not read control_port\n");
break;
}
rs->control_port = tmp;
}
else if (strcmp(token, "timing_port") == 0)
{
token = strtok_r(NULL, ";=", &ptr);
if (!token)
break;
ret = safe_atoi32(token, &tmp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not read timing_port\n");
break;
}
rs->timing_port = tmp;
}
token = strtok_r(NULL, ";=", &ptr);
}
free(transport);
if ((rs->server_port == 0) || (rs->control_port == 0) || (rs->timing_port == 0))
{
DPRINTF(E_LOG, L_RAOP, "Transport header lacked some port numbers in SETUP reply\n");
DPRINTF(E_LOG, L_RAOP, "Transport header was: %s\n", param);
goto cleanup;
}
DPRINTF(E_DBG, L_RAOP, "Negotiated AirTunes v2 UDP streaming session %s; ports s=%u c=%u t=%u\n", rs->session, rs->server_port, rs->control_port, rs->timing_port);
rs->state = RAOP_STATE_SETUP;
/* Send RECORD */
ret = raop_send_req_record(rs, raop_cb_startup_record);
if (ret < 0)
goto cleanup;
return;
cleanup:
raop_startup_cancel(rs);
}
static void
raop_cb_startup_announce(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto cleanup;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "ANNOUNCE request failed in session startup: %d %s\n", req->response_code, req->response_code_line);
goto cleanup;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto cleanup;
rs->state = RAOP_STATE_ANNOUNCE;
/* Send SETUP */
ret = raop_send_req_setup(rs, raop_cb_startup_setup);
if (ret < 0)
goto cleanup;
return;
cleanup:
raop_startup_cancel(rs);
}
static void
raop_cb_startup_options(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req || !req->response_code)
{
DPRINTF(E_LOG, L_RAOP, "No response from '%s' (%s) to OPTIONS request\n", rs->devname, rs->address);
if (rs->device->v4_address && (rs->sa.ss.ss_family == AF_INET6))
{
DPRINTF(E_LOG, L_RAOP, "Falling back to ipv4, the ipv6 address is not responding\n");
free(rs->device->v6_address);
rs->device->v6_address = NULL;
}
goto cleanup;
}
if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED))
{
DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed starting '%s': %d %s\n", rs->devname, req->response_code, req->response_code_line);
goto cleanup;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto cleanup;
if (req->response_code == RTSP_UNAUTHORIZED)
{
if (rs->req_has_auth)
{
DPRINTF(E_LOG, L_RAOP, "Bad password for device '%s'\n", rs->devname);
rs->state = RAOP_STATE_PASSWORD;
goto cleanup;
}
ret = raop_parse_auth(rs, req);
if (ret < 0)
goto cleanup;
ret = raop_send_req_options(rs, raop_cb_startup_options);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not re-run OPTIONS request with authentication\n");
goto cleanup;
}
return;
}
rs->state = RAOP_STATE_OPTIONS;
/* Send ANNOUNCE */
ret = raop_send_req_announce(rs, raop_cb_startup_announce);
if (ret < 0)
goto cleanup;
return;
cleanup:
raop_startup_cancel(rs);
}
static void
raop_cb_shutdown_teardown(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req)
goto error;
if (req->response_code != RTSP_OK)
{
DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown: %d %s\n", req->response_code, req->response_code_line);
goto error;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto error;
rs->state = RAOP_STATE_STOPPED;
/* Session shut down, tell our user */
raop_status(rs);
raop_session_cleanup(rs);
return;
error:
raop_session_failure(rs);
}
static void
raop_cb_probe_options(struct evrtsp_request *req, void *arg)
{
struct raop_session *rs;
int ret;
rs = (struct raop_session *)arg;
rs->reqs_in_flight--;
if (!req || !req->response_code)
{
DPRINTF(E_LOG, L_RAOP, "No response from '%s' (%s) to OPTIONS request\n", rs->devname, rs->address);
if (rs->device->v4_address && (rs->sa.ss.ss_family == AF_INET6))
{
DPRINTF(E_LOG, L_RAOP, "Falling back to ipv4, the ipv6 address is not responding\n");
free(rs->device->v6_address);
rs->device->v6_address = NULL;
}
goto cleanup;
}
if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED))
{
DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed probing '%s': %d %s\n", rs->devname, req->response_code, req->response_code_line);
goto cleanup;
}
ret = raop_check_cseq(rs, req);
if (ret < 0)
goto cleanup;
if (req->response_code == RTSP_UNAUTHORIZED)
{
if (rs->req_has_auth)
{
DPRINTF(E_LOG, L_RAOP, "Bad password for device '%s'\n", rs->devname);
rs->state = RAOP_STATE_PASSWORD;
goto cleanup;
}
ret = raop_parse_auth(rs, req);
if (ret < 0)
goto cleanup;
ret = raop_send_req_options(rs, raop_cb_probe_options);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not re-run OPTIONS request with authentication\n");
goto cleanup;
}
return;
}
rs->state = RAOP_STATE_OPTIONS;
/* Device probed successfully, tell our user */
raop_status(rs);
/* We're not going further with this session */
raop_session_cleanup(rs);
return;
cleanup:
raop_session_failure(rs);
}
/* RAOP devices discovery - mDNS callback */
/* Thread: main (mdns) */
/* Examples of txt content:
* Apple TV 2:
["sf=0x4" "am=AppleTV2,1" "vs=130.14" "vn=65537" "tp=UDP" "ss=16" "sr=4 4100" "sv=false" "pw=false" "md=0,1,2" "et=0,3,5" "da=true" "cn=0,1,2,3" "ch=2"]
["sf=0x4" "am=AppleTV2,1" "vs=105.5" "md=0,1,2" "tp=TCP,UDP" "vn=65537" "pw=false" "ss=16" "sr=44100" "da=true" "sv=false" "et=0,3" "cn=0,1" "ch=2" "txtvers=1"]
* Apple TV 3:
["vv=2" "vs=200.54" "vn=65537" "tp=UDP" "sf=0x44" "pk=8...f" "am=AppleTV3,1" "md=0,1,2" "ft=0x5A7FFFF7,0xE" "et=0,3,5" "da=true" "cn=0,1,2,3"]
* Apple TV 4:
["vv=2" "vs=301.44.3" "vn=65537" "tp=UDP" "pk=9...f" "am=AppleTV5,3" "md=0,1,2" "sf=0x44" "ft=0x5A7FFFF7,0x4DE" "et=0,3,5" "da=true" "cn=0,1,2,3"]
* Sony STR-DN1040:
["fv=s9327.1090.0" "am=STR-DN1040" "vs=141.9" "vn=65537" "tp=UDP" "ss=16" "sr=44100" "sv=false" "pw=false" "md=0,2" "ft=0x44F0A00" "et=0,4" "da=true" "cn=0,1" "ch=2" "txtvers=1"]
* AirFoil:
["rastx=iafs" "sm=false" "raver=3.5.3.0" "ek=1" "md=0,1,2" "ramach=Win32NT.6" "et=0,1" "cn=0,1" "sr=44100" "ss=16" "raAudioFormats=ALAC" "raflakyzeroconf=true" "pw=false" "rast=afs" "vn=3" "sv=false" "txtvers=1" "ch=2" "tp=UDP"]
* Xbmc 13:
["am=Xbmc,1" "md=0,1,2" "vs=130.14" "da=true" "vn=3" "pw=false" "sr=44100" "ss=16" "sm=false" "tp=UDP" "sv=false" "et=0,1" "ek=1" "ch=2" "cn=0,1" "txtvers=1"]
* Shairport (abrasive/1.0):
["pw=false" "txtvers=1" "vn=3" "sr=44100" "ss=16" "ch=2" "cn=0,1" "et=0,1" "ek=1" "sm=false" "tp=UDP"]
* JB2:
["fv=95.8947" "am=JB2 Gen" "vs=103.2" "tp=UDP" "vn=65537" "pw=false" "s s=16" "sr=44100" "da=true" "sv=false" "et=0,4" "cn=0,1" "ch=2" "txtvers=1"]
* Airport Express 802.11g (Gen 1):
["tp=TCP,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"]
* Airport Express 802.11n:
802.11n Gen 2 model (firmware 7.6.4): "am=Airport4,107", "et=0,1"
802.11n Gen 3 model (firmware 7.6.4): "am=Airport10,115", "et=0,4"
*/
static void
raop_device_cb(const char *name, const char *type, const char *domain, const char *hostname, int family, const char *address, int port, struct keyval *txt)
{
struct output_device *rd;
struct raop_extra *re;
cfg_t *airplay;
const char *p;
char *at_name;
char *password;
uint64_t id;
int ret;
ret = safe_hextou64(name, &id);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device ID (%s)\n", name);
return;
}
at_name = strchr(name, '@');
if (!at_name)
{
DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device name (%s)\n", name);
return;
}
at_name++;
DPRINTF(E_DBG, L_RAOP, "Event for AirPlay device %s (port %d, id %" PRIx64 ")\n", at_name, port, id);
rd = calloc(1, sizeof(struct output_device));
if (!rd)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory (rd)\n");
return;
}
re = calloc(1, sizeof(struct raop_extra));
if (!re)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory (re)\n");
free(rd);
return;
}
rd->id = id;
rd->name = strdup(at_name);
rd->type = OUTPUT_TYPE_RAOP;
rd->type_name = outputs_name(rd->type);
rd->extra_device_info = re;
if (port < 0)
{
/* Device stopped advertising */
switch (family)
{
case AF_INET:
rd->v4_port = 1;
break;
case AF_INET6:
rd->v6_port = 1;
break;
}
ret = player_device_remove(rd);
if (ret < 0)
goto free_rd;
return;
}
/* Protocol */
p = keyval_get(txt, "tp");
if (!p)
{
DPRINTF(E_LOG, L_RAOP, "AirPlay %s: no tp field in TXT record!\n", name);
goto free_rd;
}
if (*p == '\0')
{
DPRINTF(E_LOG, L_RAOP, "AirPlay %s: tp has no value\n", name);
goto free_rd;
}
if (!strstr(p, "UDP"))
{
DPRINTF(E_LOG, L_RAOP, "AirPlay %s: device does not support AirTunes v2 (tp=%s), discarding\n", name, p);
goto free_rd;
}
/* Password protection */
password = NULL;
p = keyval_get(txt, "pw");
if (!p)
{
DPRINTF(E_INFO, L_RAOP, "AirPlay %s: no pw field in TXT record, assuming no password protection\n", name);
rd->has_password = 0;
}
else if (*p == '\0')
{
DPRINTF(E_LOG, L_RAOP, "AirPlay %s: pw has no value\n", name);
goto free_rd;
}
else
{
rd->has_password = (strcmp(p, "false") != 0);
}
if (rd->has_password)
{
DPRINTF(E_LOG, L_RAOP, "AirPlay device %s is password-protected\n", name);
airplay = cfg_gettsec(cfg, "airplay", at_name);
if (airplay)
password = cfg_getstr(airplay, "password");
if (!password)
DPRINTF(E_LOG, L_RAOP, "No password given in config for AirPlay device %s\n", name);
}
rd->password = password;
/* Device type */
re->devtype = RAOP_DEV_OTHER;
p = keyval_get(txt, "am");
if (!p)
re->devtype = RAOP_DEV_APEX1_80211G; // First generation AirPort Express
else if (strncmp(p, "AirPort4", strlen("AirPort4")) == 0)
re->devtype = RAOP_DEV_APEX2_80211N; // Second generation
else if (strncmp(p, "AirPort", strlen("AirPort")) == 0)
re->devtype = RAOP_DEV_APEX3_80211N; // Third generation and newer
else if (strncmp(p, "AppleTV5,3", strlen("AppleTV5,3")) == 0)
re->devtype = RAOP_DEV_APPLETV4; // Stream to ATV with tvOS 10 needs to be kept alive
else if (strncmp(p, "AppleTV", strlen("AppleTV")) == 0)
re->devtype = RAOP_DEV_APPLETV;
else if (*p == '\0')
DPRINTF(E_LOG, L_RAOP, "AirPlay %s: am has no value\n", name);
/* Encrypt stream */
p = keyval_get(txt, "ek");
if (p && (*p == '1'))
re->encrypt = 1;
else
re->encrypt = 0;
/* Metadata support */
p = keyval_get(txt, "md");
if (p && (*p != '\0'))
re->wants_metadata = 1;
else
re->wants_metadata = 0;
rd->advertised = 1;
switch (family)
{
case AF_INET:
rd->v4_address = strdup(address);
rd->v4_port = port;
DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device %s: password: %u, encrypt: %u, metadata: %u, type %s, address %s:%d\n",
name, rd->has_password, re->encrypt, re->wants_metadata, raop_devtype[re->devtype], address, port);
break;
case AF_INET6:
rd->v6_address = strdup(address);
rd->v6_port = port;
DPRINTF(E_INFO, L_RAOP, "Adding AirPlay device %s: password: %u, encrypt: %u, metadata: %u, type %s, address [%s]:%d\n",
name, rd->has_password, re->encrypt, re->wants_metadata, raop_devtype[re->devtype], address, port);
break;
default:
DPRINTF(E_LOG, L_RAOP, "Error: AirPlay device %s has neither ipv4 og ipv6 address\n", name);
goto free_rd;
}
ret = player_device_add(rd);
if (ret < 0)
goto free_rd;
return;
free_rd:
outputs_device_free(rd);
}
static int
raop_device_probe(struct output_device *rd, output_status_cb cb)
{
struct raop_session *rs;
int ret;
/* Send an OPTIONS request to test our ability to connect to the device,
* including the need for and/or validity of the password
*/
rs = raop_session_make(rd, AF_INET6, cb);
if (rs)
{
ret = raop_send_req_options(rs, raop_cb_probe_options);
if (ret == 0)
return 0;
else
{
DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv6 (probe)\n");
raop_session_cleanup(rs);
}
}
rs = raop_session_make(rd, AF_INET, cb);
if (!rs)
return -1;
ret = raop_send_req_options(rs, raop_cb_probe_options);
if (ret < 0)
{
DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv4 (probe)\n");
raop_session_cleanup(rs);
return -1;
}
return 0;
}
static int
raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime)
{
struct raop_session *rs;
int ret;
/* Send an OPTIONS request to establish the connection
* After that, we can determine our local address and build our session URL
* for all subsequent requests.
*/
rs = raop_session_make(rd, AF_INET6, cb);
if (rs)
{
rs->start_rtptime = rtptime;
ret = raop_send_req_options(rs, raop_cb_startup_options);
if (ret == 0)
return 0;
else
{
DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv6 (start)\n");
raop_session_cleanup(rs);
}
}
rs = raop_session_make(rd, AF_INET, cb);
if (!rs)
return -1;
rs->start_rtptime = rtptime;
ret = raop_send_req_options(rs, raop_cb_startup_options);
if (ret < 0)
{
DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv4 (start)\n");
raop_session_cleanup(rs);
return -1;
}
return 0;
}
static void
raop_device_stop(struct output_session *session)
{
struct raop_session *rs = session->session;
if (!(rs->state & RAOP_STATE_F_CONNECTED))
raop_session_cleanup(rs);
else
raop_send_req_teardown(rs, raop_cb_shutdown_teardown);
}
static void
raop_device_free_extra(struct output_device *device)
{
free(device->extra_device_info);
}
static void
raop_playback_start(uint64_t next_pkt, struct timespec *ts)
{
struct raop_session *rs;
event_del(flush_timer);
evtimer_add(keep_alive_timer, &keep_alive_tv);
sync_counter = 0;
for (rs = sessions; rs; rs = rs->next)
{
if (rs->state == RAOP_STATE_CONNECTED)
rs->state = RAOP_STATE_STREAMING;
}
/* Send initial playback sync */
raop_v2_control_send_sync(next_pkt, ts);
}
static void
raop_playback_stop(void)
{
struct raop_session *rs;
int ret;
evtimer_del(keep_alive_timer);
for (rs = sessions; rs; rs = rs->next)
{
ret = raop_send_req_teardown(rs, raop_cb_shutdown_teardown);
if (ret < 0)
DPRINTF(E_LOG, L_RAOP, "shutdown: TEARDOWN request failed!\n");
}
}
static void
raop_set_status_cb(struct output_session *session, output_status_cb cb)
{
struct raop_session *rs = session->session;
rs->status_cb = cb;
}
static int
raop_init(void)
{
char ebuf[64];
char *ptr;
char *libname;
gpg_error_t gc_err;
int v6enabled;
int family;
int ret;
timing_4svc.fd = -1;
timing_4svc.port = 0;
timing_6svc.fd = -1;
timing_6svc.port = 0;
control_4svc.fd = -1;
control_4svc.port = 0;
control_6svc.fd = -1;
control_6svc.port = 0;
sessions = NULL;
pktbuf_size = 0;
pktbuf_head = NULL;
pktbuf_tail = NULL;
metadata_head = NULL;
metadata_tail = NULL;
/* Generate RTP SSRC ID from library name */
libname = cfg_getstr(cfg_getsec(cfg, "library"), "name");
ssrc_id = djb_hash(libname, strlen(libname));
/* Random RTP sequence start */
gcry_randomize(&stream_seq, sizeof(stream_seq), GCRY_STRONG_RANDOM);
/* Generate AES key and IV */
gcry_randomize(raop_aes_key, sizeof(raop_aes_key), GCRY_STRONG_RANDOM);
gcry_randomize(raop_aes_iv, sizeof(raop_aes_iv), GCRY_STRONG_RANDOM);
/* Setup AES */
gc_err = gcry_cipher_open(&raop_aes_ctx, GCRY_CIPHER_AES, GCRY_CIPHER_MODE_CBC, 0);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not open AES cipher: %s\n", ebuf);
return -1;
}
/* Set key */
gc_err = gcry_cipher_setkey(raop_aes_ctx, raop_aes_key, sizeof(raop_aes_key));
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_RAOP, "Could not set AES key: %s\n", ebuf);
goto out_close_cipher;
}
/* Prepare Base64-encoded key & IV for SDP */
raop_aes_key_b64 = raop_crypt_encrypt_aes_key_base64();
if (!raop_aes_key_b64)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't encrypt and encode AES session key\n");
goto out_close_cipher;
}
raop_aes_iv_b64 = b64_encode(raop_aes_iv, sizeof(raop_aes_iv));
if (!raop_aes_iv_b64)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't encode AES IV\n");
goto out_free_b64_key;
}
/* Remove base64 padding */
ptr = strchr(raop_aes_key_b64, '=');
if (ptr)
*ptr = '\0';
ptr = strchr(raop_aes_iv_b64, '=');
if (ptr)
*ptr = '\0';
flush_timer = evtimer_new(evbase_player, raop_flush_timer_cb, NULL);
keep_alive_timer = evtimer_new(evbase_player, raop_keep_alive_timer_cb, NULL);
if (!flush_timer || !keep_alive_timer)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for flush timer or keep alive timer\n");
goto out_free_b64_iv;
}
v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6");
ret = raop_v2_timing_start(v6enabled);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "AirPlay time synchronization failed to start\n");
goto out_free_timers;
}
ret = raop_v2_control_start(v6enabled);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "AirPlay playback control failed to start\n");
goto out_stop_timing;
}
if (v6enabled)
v6enabled = !((timing_6svc.fd < 0) || (control_6svc.fd < 0));
if (v6enabled)
family = AF_UNSPEC;
else
family = AF_INET;
ret = mdns_browse("_raop._tcp", family, raop_device_cb);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not add mDNS browser for AirPlay devices\n");
goto out_stop_control;
}
return 0;
out_stop_control:
raop_v2_control_stop();
out_stop_timing:
raop_v2_timing_stop();
out_free_timers:
event_free(flush_timer);
event_free(keep_alive_timer);
out_free_b64_iv:
free(raop_aes_iv_b64);
out_free_b64_key:
free(raop_aes_key_b64);
out_close_cipher:
gcry_cipher_close(raop_aes_ctx);
return -1;
}
static void
raop_deinit(void)
{
struct raop_session *rs;
for (rs = sessions; sessions; rs = sessions)
{
sessions = rs->next;
raop_session_free(rs);
}
raop_v2_control_stop();
raop_v2_timing_stop();
event_free(flush_timer);
event_free(keep_alive_timer);
gcry_cipher_close(raop_aes_ctx);
free(raop_aes_key_b64);
free(raop_aes_iv_b64);
}
struct output_definition output_raop =
{
.name = "AirPlay",
.type = OUTPUT_TYPE_RAOP,
.priority = 1,
.disabled = 0,
.init = raop_init,
.deinit = raop_deinit,
.device_start = raop_device_start,
.device_stop = raop_device_stop,
.device_probe = raop_device_probe,
.device_free_extra = raop_device_free_extra,
.device_volume_set = raop_set_volume_one,
.playback_start = raop_playback_start,
.playback_stop = raop_playback_stop,
.write = raop_v2_write,
.flush = raop_flush,
.status_cb = raop_set_status_cb,
.metadata_prepare = raop_metadata_prepare,
.metadata_send = raop_metadata_send,
.metadata_purge = raop_metadata_purge,
.metadata_prune = raop_metadata_prune,
};