owntone-server/src/raop.c

4141 lines
89 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>
#if defined(__linux__) || defined(__GLIBC__)
# include <endian.h>
# include <byteswap.h>
#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
# include <sys/endian.h>
#endif
#include <arpa/inet.h>
#include <net/if.h>
#include <event.h>
#include "evrtsp/evrtsp.h"
#include <gcrypt.h>
#include "conffile.h"
#include "logger.h"
#include "misc.h"
#include "player.h"
#include "db.h"
#include "artwork.h"
#include "dmap_common.h"
#include "raop.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
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;
};
struct raop_session
{
struct evrtsp_connection *ctrl;
enum raop_session_state state;
unsigned req_has_auth:1;
unsigned encrypt:1;
unsigned auth_quirk_itunes:1;
unsigned wants_metadata:1;
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 raop_device *dev;
raop_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;
};
struct raop_deferred_eh
{
struct event ev;
struct raop_session *session;
};
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";
/* 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;
/* 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);
}
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;
}
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);
}
}
static struct raop_metadata *
raop_metadata_prepare(int id, uint64_t rtptime)
{
struct query_params qp;
struct db_media_file_info dbmfi;
char filter[32];
struct raop_metadata *rmd;
struct evbuffer *tmp;
uint64_t duration;
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));
/* Get dbmfi */
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_ITEMS;
qp.idx_type = I_NONE;
qp.sort = S_NONE;
qp.filter = filter;
ret = snprintf(filter, sizeof(filter), "id = %d", id);
if ((ret < 0) || (ret >= sizeof(filter)))
{
DPRINTF(E_LOG, L_RAOP, "Could not build filter for file id %d; metadata will not be sent\n", id);
goto out_rmd;
}
ret = db_query_start(&qp);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't start query; no metadata will be sent\n");
goto out_rmd;
}
ret = db_query_fetch_file(&qp, &dbmfi);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't fetch file id %d; metadata will not be sent\n", id);
goto out_query;
}
/* 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_query;
}
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_query;
}
ret = dmap_encode_file_metadata(rmd->metadata, tmp, &dbmfi, NULL, 0, 0, 1);
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 */
ret = safe_atou64(dbmfi.song_length, &duration);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Failed to convert song_length to integer; no metadata will be sent\n");
goto out_metadata;
}
rmd->start = rtptime;
rmd->end = rtptime + (duration * 44100UL) / 1000UL;
/* 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_filename(dbmfi.path, 600, 600, ART_CAN_PNG | ART_CAN_JPEG, rmd->artwork);
if (ret < 0)
{
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for '%s' (%d); no artwork will be sent\n", dbmfi.title, id);
evbuffer_free(rmd->artwork);
rmd->artwork = NULL;
}
rmd->artwork_fmt = ret;
skip_artwork:
db_query_end(&qp);
/* Add rmd to metadata list */
if (metadata_tail)
metadata_tail->next = rmd;
else
{
metadata_head = rmd;
metadata_tail = rmd;
}
return rmd;
out_metadata:
evbuffer_free(rmd->metadata);
out_query:
db_query_end(&qp);
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_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;
}
static int
raop_grab_cseq(struct evkeyvalq *headers)
{
const char *param;
int cseq;
int ret;
param = evrtsp_find_header(headers, "CSeq");
if (!param)
return -1;
ret = safe_atoi32(param, &cseq);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Could not convert CSeq value to integer (%s)\n", param);
return -1;
}
return cseq;
}
static int
raop_check_cseq(struct raop_session *rs, struct evrtsp_request *req)
{
int reply_cseq;
int request_cseq;
reply_cseq = raop_grab_cseq(req->input_headers);
/* AirFoil won't return cseq, so skip the check */
if (reply_cseq < 0)
{
DPRINTF(E_INFO, L_RAOP, "No CSeq in reply, skipping check\n");
return 0;
}
request_cseq = raop_grab_cseq(req->output_headers);
if (request_cseq < 0)
{
DPRINTF(E_LOG, L_RAOP, "No CSeq in request\n");
return -1;
}
if (reply_cseq == request_cseq)
return 0;
DPRINTF(E_LOG, L_RAOP, "Reply CSeq does not match request CSeq: got %d expected %d\n", reply_cseq, request_cseq);
return -1;
}
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->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;
}
static void
raop_session_free(struct raop_session *rs)
{
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
evrtsp_connection_free(rs->ctrl);
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);
}
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_PASSWORD)
rs->state = RAOP_FAILED;
rs->status_cb(rs->dev, rs, rs->state);
raop_session_cleanup(rs);
}
static void
raop_deferred_eh_cb(int fd, short what, void *arg)
{
struct raop_deferred_eh *deh;
struct raop_session *rs;
deh = (struct raop_deferred_eh *)arg;
rs = deh->session;
free(deh);
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_deferred_eh *deh;
struct raop_session *rs;
rs = (struct raop_session *)arg;
DPRINTF(E_LOG, L_RAOP, "ApEx %s closed RTSP connection\n", rs->devname);
rs->state = RAOP_FAILED;
deh = (struct raop_deferred_eh *)malloc(sizeof(struct raop_deferred_eh));
if (!deh)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for deferred error handling!\n");
return;
}
deh->session = rs;
evtimer_set(&deh->ev, raop_deferred_eh_cb, deh);
event_base_set(evbase_player, &deh->ev);
evutil_timerclear(&tv);
evtimer_add(&deh->ev, &tv);
}
static struct raop_session *
raop_session_make(struct raop_device *rd, int family, raop_status_cb cb)
{
struct raop_session *rs;
char *address;
char *intf;
unsigned short port;
int ret;
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;
}
rs = (struct raop_session *)malloc(sizeof(struct raop_session));
if (!rs)
{
DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP session\n");
return NULL;
}
memset(rs, 0, sizeof(struct raop_session));
rs->state = RAOP_STOPPED;
rs->reqs_in_flight = 0;
rs->cseq = 1;
rs->dev = rd;
rs->status_cb = cb;
rs->server_fd = -1;
rs->password = rd->password;
rs->wants_metadata = rd->wants_metadata;
switch (rd->devtype)
{
case RAOP_DEV_APEX1_80211G:
rs->encrypt = 1;
rs->auth_quirk_itunes = 1;
break;
case RAOP_DEV_APEX2_80211N:
rs->encrypt = 1;
rs->auth_quirk_itunes = 0;
break;
case RAOP_DEV_APEX3_80211N:
rs->encrypt = 0;
rs->auth_quirk_itunes = 0;
break;
case RAOP_DEV_APPLETV:
rs->encrypt = 0;
rs->auth_quirk_itunes = 0;
break;
case RAOP_DEV_OTHER:
rs->encrypt = rd->encrypt;
rs->auth_quirk_itunes = 0;
break;
}
rs->ctrl = evrtsp_connection_new(address, port);
if (!rs->ctrl)
{
DPRINTF(E_LOG, L_RAOP, "Could not create control connection to %s\n", address);
goto out_free_rs;
}
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 address not valid (%s)\n", address);
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_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;
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;
}
ret = evbuffer_add(evbuf, EVBUFFER_DATA(rmd->artwork), EVBUFFER_LENGTH(rmd->artwork));
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)
{
int ret;
ret = evbuffer_add(evbuf, EVBUFFER_DATA(rmd->metadata), EVBUFFER_LENGTH(rmd->metadata));
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;
}
}
}
}
void
raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup)
{
struct raop_session *rs;
struct raop_metadata *rmd;
uint32_t delay;
int ret;
rmd = raop_metadata_prepare(id, rtptime);
if (!rmd)
return;
for (rs = sessions; rs; rs = rs->next)
{
if (!(rs->state & RAOP_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;
}
DPRINTF(E_DBG, L_RAOP, "Setting max_volume for device %s to %d\n", name, max_volume);
/* RAOP volume
* -144.0 is off
* 0 - -30.0 maps to 100 - 0
*/
if (volume == 0)
raop_volume = -144.0;
else
raop_volume = -30.0 + ((float)max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME);
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 */
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)
{
raop_status_cb status_cb;
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 */
status_cb = rs->status_cb;
rs->status_cb = NULL;
status_cb(rs->dev, rs, rs->state);
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] */
int
raop_set_volume_one(struct raop_session *rs, int volume, raop_status_cb cb)
{
int ret;
if (!(rs->state & RAOP_F_CONNECTED))
return 0;
ret = raop_set_volume_internal(rs, 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)
{
raop_status_cb status_cb;
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_CONNECTED;
/* Let our user know */
status_cb = rs->status_cb;
rs->status_cb = NULL;
status_cb(rs->dev, rs, rs->state);
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_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_F_CONNECTED))
continue;
raop_device_stop(rs);
}
}
int
raop_flush(raop_status_cb cb, uint64_t rtptime)
{
struct timeval tv;
struct raop_session *rs;
int pending;
int ret;
pending = 0;
for (rs = sessions; rs; rs = rs->next)
{
if (rs->state != RAOP_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)
{
evtimer_set(&flush_timer, raop_flush_timer_cb, NULL);
event_base_set(evbase_player, &flush_timer);
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)
{
char address[INET6_ADDRSTRLEN];
union sockaddr_all sa;
uint8_t req[32];
uint8_t res[32];
struct ntp_stamp recv_stamp;
struct ntp_stamp xmit_stamp;
struct raop_session *rs;
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;
}
switch (sa.ss.ss_family)
{
case AF_INET:
if (svc != &timing_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 != &timing_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, "Time sync: Unknown address family %d\n", sa.ss.ss_family);
goto readd;
}
if (!rs)
{
if (!ret)
DPRINTF(E_LOG, L_RAOP, "Time sync request from [error: %s]; not a RAOP client\n", strerror(errno));
else
DPRINTF(E_LOG, L_RAOP, "Time sync request from %s; not a RAOP client\n", address);
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 __linux__
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;
}
event_set(&svc->ev, svc->fd, EV_READ, raop_v2_timing_cb, svc);
event_base_set(evbase_player, &svc->ev);
ret = event_add(&svc->ev, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't add event for timing requests\n");
goto out_fail;
}
return 0;
out_fail:
close(svc->fd);
svc->fd = -1;
svc->port = 0;
return -1;
}
static void
raop_v2_timing_stop(void)
{
if (event_initialized(&timing_4svc.ev))
event_del(&timing_4svc.ev);
if (event_initialized(&timing_6svc.ev))
event_del(&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_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 __linux__
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;
}
event_set(&svc->ev, svc->fd, EV_READ, raop_v2_control_cb, svc);
event_base_set(evbase_player, &svc->ev);
ret = event_add(&svc->ev, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "Couldn't add event for control requests\n");
goto out_fail;
}
return 0;
out_fail:
close(svc->fd);
svc->fd = -1;
svc->port = 0;
return -1;
}
static void
raop_v2_control_stop(void)
{
if (event_initialized(&control_4svc.ev))
event_del(&control_4svc.ev);
if (event_initialized(&control_6svc.ev))
event_del(&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;
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;
}
void
raop_v2_write(uint8_t *buf, uint64_t rtptime)
{
struct raop_v2_packet *pkt;
struct raop_session *rs;
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 = rs->next)
{
if (rs->state != RAOP_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, "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 __linux__
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_STREAMING;
else
rs->state = RAOP_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)
{
raop_status_cb status_cb;
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 */
status_cb = rs->status_cb;
rs->status_cb = NULL;
status_cb(rs->dev, rs, rs->state);
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_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_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_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)
goto cleanup;
if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED))
{
DPRINTF(E_LOG, L_RAOP, "OPTIONS 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;
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_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_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)
{
raop_status_cb status_cb;
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_STOPPED;
/* Session shut down, tell our user */
status_cb = rs->status_cb;
rs->status_cb = NULL;
status_cb(rs->dev, rs, rs->state);
raop_session_cleanup(rs);
return;
error:
raop_session_failure(rs);
}
static void
raop_cb_probe_options(struct evrtsp_request *req, void *arg)
{
raop_status_cb status_cb;
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) && (req->response_code != RTSP_UNAUTHORIZED))
{
DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed in device probe: %d %s\n", 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_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_OPTIONS;
/* Device probed successfully, tell our user */
status_cb = rs->status_cb;
rs->status_cb = NULL;
status_cb(rs->dev, rs, rs->state);
/* We're not going further with this session */
raop_session_cleanup(rs);
return;
cleanup:
raop_session_failure(rs);
}
int
raop_device_probe(struct raop_device *rd, raop_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;
}
int
raop_device_start(struct raop_device *rd, raop_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;
}
void
raop_device_stop(struct raop_session *rs)
{
if (!(rs->state & RAOP_F_CONNECTED))
raop_session_cleanup(rs);
else
raop_send_req_teardown(rs, raop_cb_shutdown_teardown);
}
void
raop_playback_start(uint64_t next_pkt, struct timespec *ts)
{
struct raop_session *rs;
if (event_initialized(&flush_timer))
event_del(&flush_timer);
sync_counter = 0;
for (rs = sessions; rs; rs = rs->next)
{
if (rs->state == RAOP_CONNECTED)
rs->state = RAOP_STREAMING;
}
/* Send initial playback sync */
raop_v2_control_send_sync(next_pkt, ts);
}
void
raop_playback_stop(void)
{
struct raop_session *rs;
int ret;
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");
}
}
void
raop_set_status_cb(struct raop_session *rs, raop_status_cb cb)
{
rs->status_cb = cb;
}
int
raop_init(int *v6enabled)
{
char ebuf[64];
char *ptr;
char *libname;
gpg_error_t gc_err;
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';
ret = raop_v2_timing_start(*v6enabled);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "AirTunes v2 time synchronization failed to start\n");
goto out_free_b64_iv;
}
ret = raop_v2_control_start(*v6enabled);
if (ret < 0)
{
DPRINTF(E_LOG, L_RAOP, "AirTunes v2 playback synchronization failed to start\n");
goto out_stop_timing;
}
if (*v6enabled)
*v6enabled = !((timing_6svc.fd < 0) || (control_6svc.fd < 0));
return 0;
out_stop_timing:
raop_v2_timing_stop();
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;
}
void
raop_deinit(void)
{
struct raop_session *rs;
for (rs = sessions; sessions; rs = sessions)
{
sessions = rs->next;
raop_session_free(rs);
}
raop_v2_timing_stop();
raop_v2_control_stop();
gcry_cipher_close(raop_aes_ctx);
free(raop_aes_key_b64);
free(raop_aes_iv_b64);
}