mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-01 01:53:23 -05:00
c96dc1fcff
Closes #613
4613 lines
116 KiB
C
4613 lines
116 KiB
C
/*
|
|
* Copyright (C) 2012-2020 Espen Jürgensen <espenjurgensen@gmail.com>
|
|
* 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
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdbool.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>
|
|
|
|
#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 "rtp_common.h"
|
|
#include "outputs.h"
|
|
#include "pair_ap/pair.h"
|
|
|
|
#define ALAC_HEADER_LEN 3
|
|
|
|
#define RAOP_QUALITY_SAMPLE_RATE_DEFAULT 44100
|
|
#define RAOP_QUALITY_BITS_PER_SAMPLE_DEFAULT 16
|
|
#define RAOP_QUALITY_CHANNELS_DEFAULT 2
|
|
|
|
// AirTunes v2 number of samples per packet
|
|
// Probably using this value because 44100/352 and 48000/352 has good 32 byte
|
|
// alignment, which improves performance of some encoders
|
|
#define RAOP_SAMPLES_PER_PACKET 352
|
|
|
|
#define RAOP_RTP_PAYLOADTYPE 0x60
|
|
|
|
// How many RTP packets keep in a buffer for retransmission
|
|
#define RAOP_PACKET_BUFFER_SIZE 1000
|
|
|
|
#define RAOP_MD_DELAY_STARTUP 15360
|
|
#define RAOP_MD_DELAY_SWITCH (RAOP_MD_DELAY_STARTUP * 2)
|
|
#define RAOP_MD_WANTS_TEXT (1 << 0)
|
|
#define RAOP_MD_WANTS_ARTWORK (1 << 1)
|
|
#define RAOP_MD_WANTS_PROGRESS (1 << 2)
|
|
|
|
// ATV4 and Homepod disconnect for reasons that are not clear, but sending them
|
|
// progress metadata at regular intervals reduces the problem. The below
|
|
// interval was determined via testing, see:
|
|
// https://github.com/owntone/owntone-server/issues/734#issuecomment-622959334
|
|
#define RAOP_KEEP_ALIVE_INTERVAL 25
|
|
|
|
// This is an arbitrary value which just needs to be kept in sync with the config
|
|
#define RAOP_CONFIG_MAX_VOLUME 11
|
|
|
|
enum raop_devtype {
|
|
RAOP_DEV_APEX1_80211G,
|
|
RAOP_DEV_APEX2_80211N,
|
|
RAOP_DEV_APEX3_80211N,
|
|
RAOP_DEV_APPLETV,
|
|
RAOP_DEV_APPLETV4,
|
|
RAOP_DEV_HOMEPOD,
|
|
RAOP_DEV_OTHER,
|
|
};
|
|
|
|
// Session is starting up
|
|
#define RAOP_STATE_F_STARTUP (1 << 13)
|
|
// Streaming is up (connection established)
|
|
#define RAOP_STATE_F_CONNECTED (1 << 14)
|
|
// Couldn't start device
|
|
#define RAOP_STATE_F_FAILED (1 << 15)
|
|
|
|
enum raop_state {
|
|
// Device is stopped (no session)
|
|
RAOP_STATE_STOPPED = 0,
|
|
// Session startup
|
|
RAOP_STATE_STARTUP = RAOP_STATE_F_STARTUP | 0x01,
|
|
RAOP_STATE_OPTIONS = RAOP_STATE_F_STARTUP | 0x02,
|
|
RAOP_STATE_ANNOUNCE = RAOP_STATE_F_STARTUP | 0x03,
|
|
RAOP_STATE_SETUP = RAOP_STATE_F_STARTUP | 0x04,
|
|
RAOP_STATE_RECORD = RAOP_STATE_F_STARTUP | 0x05,
|
|
// Session established
|
|
// - streaming ready (RECORD sent and acked, connection established)
|
|
// - commands (SET_PARAMETER) are possible
|
|
RAOP_STATE_CONNECTED = RAOP_STATE_F_CONNECTED | 0x01,
|
|
// Media data is being sent
|
|
RAOP_STATE_STREAMING = RAOP_STATE_F_CONNECTED | 0x02,
|
|
// Session teardown in progress (-> going to STOPPED state)
|
|
RAOP_STATE_TEARDOWN = RAOP_STATE_F_CONNECTED | 0x03,
|
|
// Session is failed, couldn't startup or error occurred
|
|
RAOP_STATE_FAILED = RAOP_STATE_F_FAILED | 0x01,
|
|
// Password issue: unknown password or bad password, or pending PIN from user
|
|
RAOP_STATE_PASSWORD = RAOP_STATE_F_FAILED | 0x02,
|
|
};
|
|
|
|
// Info about the device, which is not required by the player, only internally
|
|
struct raop_extra
|
|
{
|
|
enum raop_devtype devtype;
|
|
|
|
uint16_t wanted_metadata;
|
|
bool encrypt;
|
|
bool supports_auth_setup;
|
|
};
|
|
|
|
struct raop_master_session
|
|
{
|
|
struct evbuffer *evbuf;
|
|
int evbuf_samples;
|
|
|
|
struct rtp_session *rtp_session;
|
|
|
|
struct rtcp_timestamp cur_stamp;
|
|
|
|
uint8_t *rawbuf;
|
|
size_t rawbuf_size;
|
|
int samples_per_packet;
|
|
bool encrypt;
|
|
|
|
// Number of samples that we tell the output to buffer (this will mean that
|
|
// the position that we send in the sync packages are offset by this amount
|
|
// compared to the rtptimes of the corresponding RTP packages we are sending)
|
|
int output_buffer_samples;
|
|
|
|
struct raop_master_session *next;
|
|
};
|
|
|
|
struct raop_session
|
|
{
|
|
uint64_t device_id;
|
|
int callback_id;
|
|
|
|
struct raop_master_session *master_session;
|
|
|
|
struct evrtsp_connection *ctrl;
|
|
|
|
enum raop_state state;
|
|
|
|
uint16_t wanted_metadata;
|
|
bool req_has_auth;
|
|
bool encrypt;
|
|
bool auth_quirk_itunes;
|
|
bool supports_post;
|
|
bool supports_auth_setup;
|
|
|
|
bool only_probe;
|
|
|
|
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 family;
|
|
|
|
union net_sockaddr naddr;
|
|
|
|
int volume;
|
|
|
|
/* AirTunes v2 */
|
|
unsigned short server_port;
|
|
unsigned short control_port;
|
|
unsigned short timing_port; // ATV4 has this set to 0, but it is not used by us anyway
|
|
|
|
/* Device verification, see pair.h */
|
|
struct pair_verify_context *pair_verify_ctx;
|
|
struct pair_setup_context *pair_setup_ctx;
|
|
|
|
int server_fd;
|
|
|
|
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;
|
|
};
|
|
|
|
struct raop_service
|
|
{
|
|
int fd;
|
|
unsigned short port;
|
|
struct event *ev;
|
|
};
|
|
|
|
typedef void (*evrtsp_req_cb)(struct evrtsp_request *req, void *arg);
|
|
|
|
/* NTP timestamp definitions */
|
|
#define FRAC 4294967296. /* 2^32 as a double */
|
|
#define NTP_EPOCH_DELTA 0x83aa7e80 /* 2208988800 - that's 1970 - 1900 in seconds */
|
|
|
|
// TODO move to rtp_common
|
|
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";
|
|
|
|
static const uint8_t raop_auth_setup_pubkey[] =
|
|
"\x59\x02\xed\xe9\x0d\x4e\xf2\xbd\x4c\xb6\x8a\x63\x30\x03\x82\x07"
|
|
"\xa9\x4d\xbd\x50\xd8\xaa\x46\x5b\x5d\x8c\x01\x2a\x0c\x7e\x1d\x4e";
|
|
|
|
|
|
/* 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",
|
|
"HomePod",
|
|
"Other",
|
|
};
|
|
|
|
/* Struct with default quality levels */
|
|
static struct media_quality raop_quality_default =
|
|
{
|
|
RAOP_QUALITY_SAMPLE_RATE_DEFAULT,
|
|
RAOP_QUALITY_BITS_PER_SAMPLE_DEFAULT,
|
|
RAOP_QUALITY_CHANNELS_DEFAULT
|
|
};
|
|
|
|
/* 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 raop_timing_svc;
|
|
|
|
/* AirTunes v2 playback synchronization / control */
|
|
static struct raop_service raop_control_svc;
|
|
|
|
/* Metadata */
|
|
static struct output_metadata *raop_cur_metadata;
|
|
|
|
/* Keep-alive timer - hack for ATV's with tvOS 10 */
|
|
static struct event *keep_alive_timer;
|
|
static struct timeval keep_alive_tv = { RAOP_KEEP_ALIVE_INTERVAL, 0 };
|
|
|
|
/* Sessions */
|
|
static struct raop_master_session *raop_master_sessions;
|
|
static struct raop_session *raop_sessions;
|
|
|
|
// Forwards
|
|
static int
|
|
raop_device_start(struct output_device *rd, int callback_id);
|
|
|
|
|
|
/* ------------------------------- MISC HELPERS ----------------------------- */
|
|
|
|
/* 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 *dst, uint8_t *raw, int len)
|
|
{
|
|
uint8_t *maxraw;
|
|
int bpos;
|
|
|
|
bpos = 0;
|
|
maxraw = raw + len;
|
|
|
|
alac_write_bits(&dst, 1, 3, &bpos); /* channel=1, stereo */
|
|
alac_write_bits(&dst, 0, 4, &bpos); /* unknown */
|
|
alac_write_bits(&dst, 0, 8, &bpos); /* unknown */
|
|
alac_write_bits(&dst, 0, 4, &bpos); /* unknown */
|
|
alac_write_bits(&dst, 0, 1, &bpos); /* hassize */
|
|
|
|
alac_write_bits(&dst, 0, 2, &bpos); /* unused */
|
|
alac_write_bits(&dst, 1, 1, &bpos); /* is-not-compressed */
|
|
|
|
for (; raw < maxraw; raw += 4)
|
|
{
|
|
/* Byteswap to big endian */
|
|
alac_write_bits(&dst, *(raw + 1), 8, &bpos);
|
|
alac_write_bits(&dst, *raw, 8, &bpos);
|
|
alac_write_bits(&dst, *(raw + 3), 8, &bpos);
|
|
alac_write_bits(&dst, *(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
|
|
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;
|
|
}
|
|
|
|
|
|
/* ------------------ Helpers for sending RAOP/RTSP requests ---------------- */
|
|
|
|
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;
|
|
const char *user_agent;
|
|
int ret;
|
|
|
|
method = evrtsp_method(req_method);
|
|
|
|
snprintf(buf, sizeof(buf), "%d", rs->cseq);
|
|
evrtsp_add_header(req->output_headers, "CSeq", buf);
|
|
|
|
rs->cseq++;
|
|
|
|
user_agent = cfg_getstr(cfg_getsec(cfg, "general"), "user_agent");
|
|
evrtsp_add_header(req->output_headers, "User-Agent", user_agent);
|
|
|
|
/* 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);
|
|
|
|
// We set Active-Remote as 32 bit unsigned decimal, as at least my device
|
|
// can't handle any larger. Must be aligned with volume_byactiveremote().
|
|
snprintf(buf, sizeof(buf), "%" PRIu32, (uint32_t)rs->device_id);
|
|
evrtsp_add_header(req->output_headers, "Active-Remote", 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, int family, uint32_t session_id)
|
|
{
|
|
#define SDP_PLD_FMT \
|
|
"v=0\r\n" \
|
|
"o=iTunes %u 0 IN %s %s\r\n" \
|
|
"s=iTunes\r\n" \
|
|
"c=IN %s %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 %s %s\r\n" \
|
|
"s=iTunes\r\n" \
|
|
"c=IN %s %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"
|
|
|
|
const char *af;
|
|
const char *rs_af;
|
|
char *p;
|
|
int ret;
|
|
|
|
af = (family == AF_INET) ? "IP4" : "IP6";
|
|
rs_af = (rs->family == AF_INET) ? "IP4" : "IP6";
|
|
|
|
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, af, address, rs_af, rs->address, RAOP_SAMPLES_PER_PACKET,
|
|
raop_aes_key_b64, raop_aes_iv_b64);
|
|
else
|
|
ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT_NO_ENC,
|
|
session_id, af, address, rs_af, rs->address, RAOP_SAMPLES_PER_PACKET);
|
|
|
|
if (p)
|
|
*p = '%';
|
|
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Out of memory for SDP payload\n");
|
|
return -1;
|
|
}
|
|
|
|
DPRINTF(E_INFO, L_RAOP, "Setting up AirPlay session %u (%s -> %s)\n", session_id, address, rs->address);
|
|
|
|
return 0;
|
|
|
|
#undef SDP_PLD_FMT
|
|
#undef SDP_PLD_FMT_NO_ENC
|
|
}
|
|
|
|
|
|
/* ----------------- Handlers for sending 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 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, const char *log_caller)
|
|
{
|
|
struct evrtsp_request *req;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending TEARDOWN to '%s'\n", log_caller, rs->devname);
|
|
|
|
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 to '%s'\n", rs->devname);
|
|
|
|
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, evrtsp_req_cb cb, const char *log_caller)
|
|
{
|
|
struct raop_master_session *rms = rs->master_session;
|
|
struct evrtsp_request *req;
|
|
char buf[64];
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending FLUSH to '%s'\n", log_caller, rs->devname);
|
|
|
|
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 */
|
|
ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", rms->rtp_session->seqnum, rms->rtp_session->pos);
|
|
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 to '%s'\n", rs->devname);
|
|
|
|
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, const char *log_caller)
|
|
{
|
|
struct evrtsp_request *req;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending SET_PARAMETER to '%s'\n", log_caller, rs->devname);
|
|
|
|
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 to '%s'\n", rs->devname);
|
|
|
|
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, const char *log_caller)
|
|
{
|
|
struct raop_master_session *rms = rs->master_session;
|
|
struct evrtsp_request *req;
|
|
char buf[64];
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending RECORD to '%s'\n", log_caller, rs->devname);
|
|
|
|
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=%" PRIu16 ";rtptime=%u", rms->rtp_session->seqnum, rms->rtp_session->pos);
|
|
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);
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "RTP-Info is %s\n", 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 to '%s'\n", rs->devname);
|
|
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
raop_send_req_setup(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller)
|
|
{
|
|
char hdr[128];
|
|
struct evrtsp_request *req;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending SETUP to '%s'\n", log_caller, rs->devname);
|
|
|
|
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 to '%s'\n", rs->devname);
|
|
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
raop_send_req_announce(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller)
|
|
{
|
|
uint8_t challenge[16];
|
|
char *challenge_b64;
|
|
char *ptr;
|
|
struct evrtsp_request *req;
|
|
char *address;
|
|
char *intf;
|
|
unsigned short port;
|
|
int family;
|
|
uint32_t session_id;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending ANNOUNCE to '%s'\n", log_caller, rs->devname);
|
|
|
|
/* Determine local address, needed for SDP and session URL */
|
|
evrtsp_connection_get_local_address(rs->ctrl, &address, &port, &family);
|
|
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);
|
|
|
|
if (family == AF_INET)
|
|
ret = snprintf(rs->session_url, sizeof(rs->session_url), "rtsp://%s/%u", address, session_id);
|
|
else
|
|
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, family, 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 to '%s'\n", rs->devname);
|
|
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
return 0;
|
|
|
|
cleanup_req:
|
|
evrtsp_request_free(req);
|
|
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
The purpose of auth-setup is to authenticate the device and to exchange keys
|
|
for encryption. We don't do that, but some AirPlay 2 speakers (Sonos beam,
|
|
Airport Express fw 7.8) require this step anyway, otherwise we get a 403 to
|
|
our ANNOUNCE. So we do it with a flag for no encryption, and without actually
|
|
authenticating the device.
|
|
|
|
Good to know (source Apple's MFi Accessory Interface Specification):
|
|
- Curve25519 Elliptic-Curve Diffie-Hellman technology for key exchange
|
|
- RSA for signing and verifying and AES-128 in counter mode for encryption
|
|
- We start by sending a Curve25519 public key + no encryption flag
|
|
- The device responds with public key, MFi certificate and a signature, which
|
|
is created by the device signing the two public keys with its RSA private
|
|
key and then encrypting the result with the AES master key derived from the
|
|
Curve25519 shared secret (generated from device private key and our public
|
|
key)
|
|
- The AES key derived from the Curve25519 shared secret can then be used to
|
|
encrypt future content
|
|
- New keys should be generated for each authentication attempt, but we don't
|
|
do that because we don't really use this + it adds a libsodium dependency
|
|
|
|
Since we don't do auth or encryption, we currently just ignore the reponse.
|
|
*/
|
|
static int
|
|
raop_send_req_auth_setup(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller)
|
|
{
|
|
struct evrtsp_request *req;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending auth-setup to '%s'\n", log_caller, rs->devname);
|
|
|
|
req = evrtsp_request_new(cb, rs);
|
|
if (!req)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for auth-setup\n");
|
|
return -1;
|
|
}
|
|
|
|
ret = raop_add_headers(rs, req, EVRTSP_REQ_POST);
|
|
if (ret < 0)
|
|
{
|
|
evrtsp_request_free(req);
|
|
return -1;
|
|
}
|
|
|
|
evrtsp_add_header(req->output_headers, "Content-Type", "application/octet-stream");
|
|
|
|
// Flag for no encryption. 0x10 may mean encryption.
|
|
evbuffer_add(req->output_buffer, "\x01", 1);
|
|
|
|
evbuffer_add(req->output_buffer, raop_auth_setup_pubkey, sizeof(raop_auth_setup_pubkey) - 1);
|
|
|
|
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_POST, "/auth-setup");
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not make auth-setup request to '%s'\n", rs->devname);
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
raop_send_req_options(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller)
|
|
{
|
|
struct evrtsp_request *req;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending OPTIONS to '%s'\n", log_caller, rs->devname);
|
|
|
|
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 to '%s'\n", rs->devname);
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
raop_send_req_pin_start(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller)
|
|
{
|
|
struct evrtsp_request *req;
|
|
int ret;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "%s: Sending pair-pin-start to '%s'\n", log_caller, rs->devname);
|
|
|
|
req = evrtsp_request_new(cb, rs);
|
|
if (!req)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request to '%s' for pair-pin-start\n", rs->devname);
|
|
return -1;
|
|
}
|
|
|
|
ret = raop_add_headers(rs, req, EVRTSP_REQ_POST);
|
|
if (ret < 0)
|
|
{
|
|
evrtsp_request_free(req);
|
|
return -1;
|
|
}
|
|
|
|
DPRINTF(E_LOG, L_RAOP, "Starting device verification for '%s', go to the web interface and enter PIN\n", rs->devname);
|
|
|
|
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_POST, "/pair-pin-start");
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not make pair-pin-start request\n");
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/* ------------------------------ Session handling -------------------------- */
|
|
|
|
// 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)
|
|
{
|
|
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;
|
|
case RAOP_STATE_TEARDOWN:
|
|
DPRINTF(E_LOG, L_RAOP, "Bug! raop_status() called with transitional state (TEARDOWN)\n");
|
|
state = OUTPUT_STATE_STOPPED;
|
|
break;
|
|
default:
|
|
DPRINTF(E_LOG, L_RAOP, "Bug! Unhandled state in raop_status(): %d\n", rs->state);
|
|
state = OUTPUT_STATE_FAILED;
|
|
}
|
|
|
|
outputs_cb(rs->callback_id, rs->device_id, state);
|
|
rs->callback_id = -1;
|
|
}
|
|
|
|
static struct raop_master_session *
|
|
master_session_make(struct media_quality *quality, bool encrypt)
|
|
{
|
|
struct raop_master_session *rms;
|
|
int ret;
|
|
|
|
// First check if we already have a suitable session
|
|
for (rms = raop_master_sessions; rms; rms = rms->next)
|
|
{
|
|
if (encrypt == rms->encrypt && quality_is_equal(quality, &rms->rtp_session->quality))
|
|
return rms;
|
|
}
|
|
|
|
// Let's create a master session
|
|
ret = outputs_quality_subscribe(quality);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not subscribe to required audio quality (%d/%d/%d)\n", quality->sample_rate, quality->bits_per_sample, quality->channels);
|
|
return NULL;
|
|
}
|
|
|
|
CHECK_NULL(L_RAOP, rms = calloc(1, sizeof(struct raop_master_session)));
|
|
|
|
rms->rtp_session = rtp_session_new(quality, RAOP_PACKET_BUFFER_SIZE, 0);
|
|
if (!rms->rtp_session)
|
|
{
|
|
outputs_quality_unsubscribe(quality);
|
|
free(rms);
|
|
return NULL;
|
|
}
|
|
|
|
rms->encrypt = encrypt;
|
|
rms->samples_per_packet = RAOP_SAMPLES_PER_PACKET;
|
|
rms->rawbuf_size = STOB(rms->samples_per_packet, quality->bits_per_sample, quality->channels);
|
|
rms->output_buffer_samples = OUTPUTS_BUFFER_DURATION * quality->sample_rate;
|
|
|
|
CHECK_NULL(L_RAOP, rms->rawbuf = malloc(rms->rawbuf_size));
|
|
CHECK_NULL(L_RAOP, rms->evbuf = evbuffer_new());
|
|
|
|
rms->next = raop_master_sessions;
|
|
raop_master_sessions = rms;
|
|
|
|
return rms;
|
|
}
|
|
|
|
static void
|
|
master_session_free(struct raop_master_session *rms)
|
|
{
|
|
if (!rms)
|
|
return;
|
|
|
|
outputs_quality_unsubscribe(&rms->rtp_session->quality);
|
|
rtp_session_free(rms->rtp_session);
|
|
evbuffer_free(rms->evbuf);
|
|
free(rms->rawbuf);
|
|
free(rms);
|
|
}
|
|
|
|
static void
|
|
master_session_cleanup(struct raop_master_session *rms)
|
|
{
|
|
struct raop_master_session *s;
|
|
struct raop_session *rs;
|
|
|
|
// First check if any other session is using the master session
|
|
for (rs = raop_sessions; rs; rs=rs->next)
|
|
{
|
|
if (rs->master_session == rms)
|
|
return;
|
|
}
|
|
|
|
if (rms == raop_master_sessions)
|
|
raop_master_sessions = raop_master_sessions->next;
|
|
else
|
|
{
|
|
for (s = raop_master_sessions; s && (s->next != rms); s = s->next)
|
|
; /* EMPTY */
|
|
|
|
if (!s)
|
|
DPRINTF(E_WARN, L_RAOP, "WARNING: struct raop_master_session not found in list; BUG!\n");
|
|
else
|
|
s->next = rms->next;
|
|
}
|
|
|
|
master_session_free(rms);
|
|
}
|
|
|
|
static void
|
|
session_free(struct raop_session *rs)
|
|
{
|
|
if (!rs)
|
|
return;
|
|
|
|
if (rs->master_session)
|
|
master_session_cleanup(rs->master_session);
|
|
|
|
if (rs->ctrl)
|
|
{
|
|
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
|
|
evrtsp_connection_free(rs->ctrl);
|
|
}
|
|
|
|
if (rs->deferredev)
|
|
event_free(rs->deferredev);
|
|
|
|
if (rs->server_fd >= 0)
|
|
close(rs->server_fd);
|
|
|
|
free(rs->realm);
|
|
free(rs->nonce);
|
|
free(rs->session);
|
|
free(rs->address);
|
|
free(rs->devname);
|
|
|
|
free(rs);
|
|
}
|
|
|
|
static void
|
|
session_cleanup(struct raop_session *rs)
|
|
{
|
|
struct raop_session *s;
|
|
|
|
if (rs == raop_sessions)
|
|
raop_sessions = raop_sessions->next;
|
|
else
|
|
{
|
|
for (s = raop_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;
|
|
}
|
|
|
|
outputs_device_session_remove(rs->device_id);
|
|
|
|
session_free(rs);
|
|
}
|
|
|
|
static void
|
|
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);
|
|
|
|
session_cleanup(rs);
|
|
}
|
|
|
|
static void
|
|
deferred_session_failure(struct raop_session *rs)
|
|
{
|
|
struct timeval tv;
|
|
|
|
rs->state = RAOP_STATE_FAILED;
|
|
|
|
evutil_timerclear(&tv);
|
|
evtimer_add(rs->deferredev, &tv);
|
|
}
|
|
|
|
static void
|
|
raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
|
|
DPRINTF(E_LOG, L_RAOP, "Device '%s' closed RTSP connection\n", rs->devname);
|
|
|
|
deferred_session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
session_teardown_cb(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown\n");
|
|
else 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);
|
|
|
|
rs->state = RAOP_STATE_STOPPED;
|
|
|
|
raop_status(rs);
|
|
|
|
session_cleanup(rs);
|
|
}
|
|
|
|
static int
|
|
session_teardown(struct raop_session *rs, const char *log_caller)
|
|
{
|
|
int ret;
|
|
|
|
ret = raop_send_req_teardown(rs, session_teardown_cb, log_caller);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "%s: TEARDOWN request failed!\n", log_caller);
|
|
deferred_session_failure(rs);
|
|
}
|
|
|
|
// Change state immediately so we won't write any more to the device
|
|
rs->state = RAOP_STATE_TEARDOWN;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
deferredev_cb(int fd, short what, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
|
|
if (rs->state == RAOP_STATE_FAILED)
|
|
{
|
|
DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device '%s'\n", rs->devname);
|
|
session_failure(rs);
|
|
}
|
|
else
|
|
{
|
|
DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP session on '%s'\n", rs->devname);
|
|
session_teardown(rs, "deferredev_cb");
|
|
}
|
|
}
|
|
|
|
static int
|
|
session_connection_setup(struct raop_session *rs, struct output_device *rd, int family)
|
|
{
|
|
char *address;
|
|
char *intf;
|
|
unsigned short port;
|
|
int ret;
|
|
|
|
rs->naddr.ss.ss_family = family;
|
|
|
|
switch (family)
|
|
{
|
|
case AF_INET:
|
|
if (!rd->v4_address)
|
|
return -1;
|
|
|
|
address = rd->v4_address;
|
|
port = rd->v4_port;
|
|
|
|
ret = inet_pton(AF_INET, address, &rs->naddr.sin.sin_addr);
|
|
break;
|
|
|
|
case AF_INET6:
|
|
if (!rd->v6_address)
|
|
return -1;
|
|
|
|
address = rd->v6_address;
|
|
port = rd->v6_port;
|
|
|
|
intf = strchr(address, '%');
|
|
if (intf)
|
|
*intf = '\0';
|
|
|
|
ret = inet_pton(AF_INET6, address, &rs->naddr.sin6.sin6_addr);
|
|
|
|
if (intf)
|
|
{
|
|
*intf = '%';
|
|
|
|
intf++;
|
|
|
|
rs->naddr.sin6.sin6_scope_id = if_nametoindex(intf);
|
|
if (rs->naddr.sin6.sin6_scope_id == 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not find interface %s\n", intf);
|
|
|
|
ret = -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
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");
|
|
return -1;
|
|
}
|
|
|
|
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);
|
|
return -1;
|
|
}
|
|
|
|
evrtsp_connection_set_base(rs->ctrl, evbase_player);
|
|
|
|
rs->address = strdup(address);
|
|
rs->family = family;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct raop_session *
|
|
session_find_by_address(union net_sockaddr *peer_addr)
|
|
{
|
|
struct raop_session *rs;
|
|
uint32_t *addr_ptr;
|
|
int family = peer_addr->sa.sa_family;
|
|
|
|
for (rs = raop_sessions; rs; rs = rs->next)
|
|
{
|
|
if (family == rs->family)
|
|
{
|
|
if (family == AF_INET && peer_addr->sin.sin_addr.s_addr == rs->naddr.sin.sin_addr.s_addr)
|
|
break;
|
|
|
|
if (family == AF_INET6 && IN6_ARE_ADDR_EQUAL(&peer_addr->sin6.sin6_addr, &rs->naddr.sin6.sin6_addr))
|
|
break;
|
|
}
|
|
else if (family == AF_INET6 && IN6_IS_ADDR_V4MAPPED(&peer_addr->sin6.sin6_addr))
|
|
{
|
|
// ipv4 mapped to ipv6 consists of 16 bytes/4 words: 0x00000000 0x00000000 0x0000ffff 0x[IPv4]
|
|
addr_ptr = (uint32_t *)(&peer_addr->sin6.sin6_addr);
|
|
if (addr_ptr[3] == rs->naddr.sin.sin_addr.s_addr)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return rs;
|
|
}
|
|
|
|
static struct raop_session *
|
|
session_make(struct output_device *rd, int callback_id, bool only_probe)
|
|
{
|
|
struct raop_session *rs;
|
|
struct raop_extra *re;
|
|
int ret;
|
|
|
|
re = rd->extra_device_info;
|
|
|
|
|
|
CHECK_NULL(L_RAOP, rs = calloc(1, sizeof(struct raop_session)));
|
|
CHECK_NULL(L_RAOP, rs->deferredev = evtimer_new(evbase_player, deferredev_cb, rs));
|
|
|
|
rs->devname = strdup(rd->name);
|
|
rs->volume = rd->volume;
|
|
|
|
rs->state = RAOP_STATE_STOPPED;
|
|
rs->only_probe = only_probe;
|
|
rs->reqs_in_flight = 0;
|
|
rs->cseq = 1;
|
|
|
|
rs->device_id = rd->id;
|
|
rs->callback_id = callback_id;
|
|
|
|
rs->server_fd = -1;
|
|
|
|
rs->password = rd->password;
|
|
|
|
rs->supports_auth_setup = re->supports_auth_setup;
|
|
rs->wanted_metadata = re->wanted_metadata;
|
|
|
|
switch (re->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_APPLETV4:
|
|
rs->encrypt = 0;
|
|
rs->auth_quirk_itunes = 0;
|
|
break;
|
|
|
|
default:
|
|
rs->encrypt = re->encrypt;
|
|
rs->auth_quirk_itunes = 0;
|
|
}
|
|
|
|
rs->timing_svc = &raop_timing_svc;
|
|
rs->control_svc = &raop_control_svc;
|
|
|
|
ret = session_connection_setup(rs, rd, AF_INET6);
|
|
if (ret < 0)
|
|
{
|
|
ret = session_connection_setup(rs, rd, AF_INET);
|
|
if (ret < 0)
|
|
goto error;
|
|
}
|
|
|
|
rs->master_session = master_session_make(&rd->quality, rs->encrypt);
|
|
if (!rs->master_session)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not attach a master session for device '%s'\n", rd->name);
|
|
goto error;
|
|
}
|
|
|
|
// Attach to list of sessions
|
|
rs->next = raop_sessions;
|
|
raop_sessions = rs;
|
|
|
|
// rs is now the official device session
|
|
outputs_device_session_add(rd->id, rs);
|
|
|
|
return rs;
|
|
|
|
error:
|
|
session_free(rs);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/* ----------------------------- Metadata handling -------------------------- */
|
|
|
|
static void
|
|
raop_metadata_free(struct raop_metadata *rmd)
|
|
{
|
|
if (!rmd)
|
|
return;
|
|
|
|
if (rmd->metadata)
|
|
evbuffer_free(rmd->metadata);
|
|
if (rmd->artwork)
|
|
evbuffer_free(rmd->artwork);
|
|
|
|
free(rmd);
|
|
}
|
|
|
|
static void
|
|
raop_metadata_purge(void)
|
|
{
|
|
if (!raop_cur_metadata)
|
|
return;
|
|
|
|
raop_metadata_free(raop_cur_metadata->priv);
|
|
free(raop_cur_metadata);
|
|
raop_cur_metadata = NULL;
|
|
}
|
|
|
|
// *** Thread: worker ***
|
|
static void *
|
|
raop_metadata_prepare(struct output_metadata *metadata)
|
|
{
|
|
struct db_queue_item *queue_item;
|
|
struct raop_metadata *rmd;
|
|
struct evbuffer *tmp;
|
|
int ret;
|
|
|
|
queue_item = db_queue_fetch_byitemid(metadata->item_id);
|
|
if (!queue_item)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not fetch queue item\n");
|
|
return NULL;
|
|
}
|
|
|
|
CHECK_NULL(L_RAOP, rmd = calloc(1, sizeof(struct raop_metadata)));
|
|
CHECK_NULL(L_RAOP, rmd->artwork = evbuffer_new());
|
|
CHECK_NULL(L_RAOP, rmd->metadata = evbuffer_new());
|
|
CHECK_NULL(L_RAOP, tmp = evbuffer_new());
|
|
|
|
ret = artwork_get_item(rmd->artwork, queue_item->file_id, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT, 0);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file '%s'; no artwork will be sent\n", queue_item->path);
|
|
evbuffer_free(rmd->artwork);
|
|
rmd->artwork = NULL;
|
|
}
|
|
|
|
rmd->artwork_fmt = ret;
|
|
|
|
ret = dmap_encode_queue_metadata(rmd->metadata, tmp, queue_item);
|
|
evbuffer_free(tmp);
|
|
free_queue_item(queue_item, 0);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n");
|
|
raop_metadata_free(rmd);
|
|
return NULL;
|
|
}
|
|
|
|
return rmd;
|
|
}
|
|
|
|
static void
|
|
raop_cb_metadata(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
goto error;
|
|
|
|
if (req->response_code != RTSP_OK)
|
|
DPRINTF(E_WARN, L_RAOP, "SET_PARAMETER metadata/artwork/progress request to '%s' failed (proceeding anyway): %d %s\n", rs->devname, req->response_code, req->response_code_line);
|
|
|
|
ret = raop_check_cseq(rs, req);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
/* No callback to player, 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:
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_metadata_rtptimes_get(uint32_t *start, uint32_t *display, uint32_t *pos, uint32_t *end, struct raop_master_session *rms, struct output_metadata *metadata)
|
|
{
|
|
struct rtp_session *rtp_session = rms->rtp_session;
|
|
// All the calculations with long ints to avoid surprises
|
|
int64_t sample_rate;
|
|
int64_t diff_ms;
|
|
int64_t elapsed_ms;
|
|
int64_t elapsed_samples;
|
|
int64_t len_samples;
|
|
|
|
sample_rate = rtp_session->quality.sample_rate;
|
|
|
|
// First calculate the rtptime that streaming of this item started:
|
|
// - at time metadata->pts the elapsed time was metadata->pos_ms
|
|
// - the time is now rms->cur_stamp.ts and the position is rms->cur_stamp.pos
|
|
// -> time since item started is elapsed_ms = metadata->pos_ms + (rms->cur_stamp.ts - metadata->pts)
|
|
// -> start must then be start = rms->cur_stamp.pos - elapsed_ms * sample_rate;
|
|
diff_ms = (rms->cur_stamp.ts.tv_sec - metadata->pts.tv_sec) * 1000L + (rms->cur_stamp.ts.tv_nsec - metadata->pts.tv_nsec) / 1000000L;
|
|
elapsed_ms = (int64_t)metadata->pos_ms + diff_ms;
|
|
elapsed_samples = elapsed_ms * sample_rate / 1000;
|
|
*start = rms->cur_stamp.pos - elapsed_samples;
|
|
|
|
/* DPRINTF(E_DBG, L_RAOP, "pos_ms=%u, len_ms=%u, startup=%d, metadata.pts=%ld.%09ld, player.ts=%ld.%09ld, diff_ms=%" PRIi64 ", elapsed_ms=%" PRIi64 "\n",
|
|
metadata->pos_ms, metadata->len_ms, metadata->startup, metadata->pts.tv_sec, metadata->pts.tv_nsec, rms->cur_stamp.ts.tv_sec, rms->cur_stamp.ts.tv_nsec, diff_ms, elapsed_ms);
|
|
*/
|
|
// Here's the deal with progress values:
|
|
// - 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
|
|
// TODO what if we are just sending a keep_alive?
|
|
// - pos 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
|
|
// - end is the RTP time of the last sample for this song
|
|
len_samples = (int64_t)metadata->len_ms * sample_rate / 1000;
|
|
*display = metadata->startup ? *start - RAOP_MD_DELAY_STARTUP : *start - RAOP_MD_DELAY_SWITCH;
|
|
*pos = MAX(rms->cur_stamp.pos, *start);
|
|
*end = len_samples ? *start + len_samples : *pos;
|
|
|
|
DPRINTF(E_SPAM, L_RAOP, "start=%u, display=%u, pos=%u, end=%u, rtp_session.pos=%u, cur_stamp.pos=%u\n",
|
|
*start, *display, *pos, *end, rtp_session->pos, rms->cur_stamp.pos);
|
|
}
|
|
|
|
static int
|
|
raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint32_t display, uint32_t pos, uint32_t end)
|
|
{
|
|
int ret;
|
|
|
|
ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, pos, 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, "send_progress");
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER progress request to '%s'\n", rs->devname);
|
|
|
|
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, "send_artwork");
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER artwork request to '%s'\n", rs->devname);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
raop_metadata_send_text(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, "send_text");
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER metadata request to '%s'\n", rs->devname);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
raop_metadata_send_generic(struct raop_session *rs, struct output_metadata *metadata, bool only_progress)
|
|
{
|
|
struct raop_metadata *rmd = metadata->priv;
|
|
struct evbuffer *evbuf;
|
|
uint32_t start;
|
|
uint32_t display;
|
|
uint32_t pos;
|
|
uint32_t end;
|
|
char rtptime[32];
|
|
int ret;
|
|
|
|
raop_metadata_rtptimes_get(&start, &display, &pos, &end, rs->master_session, metadata);
|
|
|
|
ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", start);
|
|
if ((ret < 0) || (ret >= sizeof(rtptime)))
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer while sending metadata\n");
|
|
return -1;
|
|
}
|
|
|
|
CHECK_NULL(L_RAOP, evbuf = evbuffer_new());
|
|
|
|
if (rs->wanted_metadata & RAOP_MD_WANTS_PROGRESS)
|
|
{
|
|
ret = raop_metadata_send_progress(rs, evbuf, rmd, display, pos, end);
|
|
if (ret < 0)
|
|
goto error;
|
|
}
|
|
|
|
if (!only_progress && (rs->wanted_metadata & RAOP_MD_WANTS_TEXT))
|
|
{
|
|
ret = raop_metadata_send_text(rs, evbuf, rmd, rtptime);
|
|
if (ret < 0)
|
|
goto error;
|
|
}
|
|
|
|
if (!only_progress && (rs->wanted_metadata & RAOP_MD_WANTS_ARTWORK) && rmd->artwork)
|
|
{
|
|
ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime);
|
|
if (ret < 0)
|
|
goto error;
|
|
}
|
|
|
|
evbuffer_free(evbuf);
|
|
return 0;
|
|
|
|
error:
|
|
evbuffer_free(evbuf);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
raop_metadata_startup_send(struct raop_session *rs)
|
|
{
|
|
if (!rs->wanted_metadata || !raop_cur_metadata)
|
|
return 0;
|
|
|
|
raop_cur_metadata->startup = true;
|
|
|
|
return raop_metadata_send_generic(rs, raop_cur_metadata, false);
|
|
}
|
|
|
|
static int
|
|
raop_metadata_keep_alive_send(struct raop_session *rs)
|
|
{
|
|
if (!rs->wanted_metadata || !raop_cur_metadata)
|
|
return 0;
|
|
|
|
raop_cur_metadata->startup = false;
|
|
|
|
return raop_metadata_send_generic(rs, raop_cur_metadata, true);
|
|
}
|
|
|
|
static void
|
|
raop_metadata_send(struct output_metadata *metadata)
|
|
{
|
|
struct raop_session *rs;
|
|
struct raop_session *next;
|
|
int ret;
|
|
|
|
for (rs = raop_sessions; rs; rs = next)
|
|
{
|
|
next = rs->next;
|
|
|
|
if (!(rs->state & RAOP_STATE_F_CONNECTED) || !rs->wanted_metadata)
|
|
continue;
|
|
|
|
ret = raop_metadata_send_generic(rs, metadata, false);
|
|
if (ret < 0)
|
|
{
|
|
session_failure(rs);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Replace current metadata with the new stuff
|
|
raop_metadata_purge();
|
|
raop_cur_metadata = metadata;
|
|
}
|
|
|
|
|
|
/* ------------------------------ Volume handling --------------------------- */
|
|
|
|
static float
|
|
raop_volume_from_pct(int volume, struct output_device *device)
|
|
{
|
|
float raop_volume;
|
|
|
|
/* RAOP volume
|
|
* -144.0 is off (not really used since we have no concept of muted/off)
|
|
* 0 - 100 maps to -30.0 - 0 (if no max_volume set)
|
|
*/
|
|
if (volume >= 0 && volume <= 100)
|
|
raop_volume = -30.0 + ((float)device->max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME);
|
|
else
|
|
raop_volume = -144.0;
|
|
|
|
return raop_volume;
|
|
}
|
|
|
|
static int
|
|
raop_volume_to_pct(struct output_device *device, const char *volstr)
|
|
{
|
|
float raop_volume;
|
|
float volume;
|
|
|
|
raop_volume = atof(volstr);
|
|
|
|
if ((raop_volume == 0.0 && volstr[0] != '0') || raop_volume > 0.0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "RAOP device volume is invalid: '%s'\n", volstr);
|
|
return -1;
|
|
}
|
|
|
|
if (raop_volume <= -30.0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// RAOP volume: -144.0 is off, -30.0 - 0 scaled by max_volume maps to 0 - 100
|
|
volume = (100.0 * (raop_volume / 30.0 + 1.0) * RAOP_CONFIG_MAX_VOLUME / (float)device->max_volume);
|
|
return MAX(0, MIN(100, (int)volume));
|
|
}
|
|
|
|
static int
|
|
raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb)
|
|
{
|
|
struct output_device *device;
|
|
struct evbuffer *evbuf;
|
|
float raop_volume;
|
|
int ret;
|
|
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not set volume, device with id %" PRIu64 " not in out list\n", rs->device_id);
|
|
|
|
return -1;
|
|
}
|
|
|
|
evbuf = evbuffer_new();
|
|
if (!evbuf)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not allocate evbuffer for volume payload\n");
|
|
|
|
return -1;
|
|
}
|
|
|
|
raop_volume = raop_volume_from_pct(volume, device);
|
|
|
|
/* 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, "volume_internal");
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for volume to '%s'\n", rs->devname);
|
|
|
|
evbuffer_free(evbuf);
|
|
|
|
rs->volume = volume;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
raop_cb_set_volume(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
goto error;
|
|
|
|
if (req->response_code != RTSP_OK)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request to '%s' failed for stream volume: %d %s\n", rs->devname, 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:
|
|
session_failure(rs);
|
|
}
|
|
|
|
/* Volume in [0 - 100] */
|
|
static int
|
|
raop_set_volume_one(struct output_device *device, int callback_id)
|
|
{
|
|
struct raop_session *rs = device->session;
|
|
int ret;
|
|
|
|
if (!rs || !(rs->state & RAOP_STATE_F_CONNECTED))
|
|
return 0;
|
|
|
|
ret = raop_set_volume_internal(rs, device->volume, raop_cb_set_volume);
|
|
if (ret < 0)
|
|
{
|
|
session_failure(rs);
|
|
|
|
return 0;
|
|
}
|
|
|
|
rs->callback_id = callback_id;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
raop_cb_flush(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
goto error;
|
|
|
|
if (req->response_code != RTSP_OK)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "FLUSH request to '%s' failed: %d %s\n", rs->devname, 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:
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_keep_alive_timer_cb(int fd, short what, void *arg)
|
|
{
|
|
struct raop_session *rs;
|
|
|
|
if (!raop_sessions)
|
|
{
|
|
event_del(keep_alive_timer);
|
|
return;
|
|
}
|
|
|
|
for (rs = raop_sessions; rs; rs = rs->next)
|
|
{
|
|
if (!(rs->state & RAOP_STATE_F_CONNECTED))
|
|
continue;
|
|
|
|
raop_metadata_keep_alive_send(rs);
|
|
}
|
|
|
|
evtimer_add(keep_alive_timer, &keep_alive_tv);
|
|
}
|
|
|
|
|
|
/* -------------------- Creation and sending of RTP packets ---------------- */
|
|
|
|
static int
|
|
packet_prepare(struct rtp_packet *pkt, uint8_t *rawbuf, size_t rawbuf_size, bool encrypt)
|
|
{
|
|
char ebuf[64];
|
|
gpg_error_t gc_err;
|
|
|
|
alac_encode(pkt->payload, rawbuf, rawbuf_size);
|
|
|
|
if (!encrypt)
|
|
return 0;
|
|
|
|
// 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);
|
|
return -1;
|
|
}
|
|
|
|
// 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);
|
|
return -1;
|
|
}
|
|
|
|
// Encrypt in blocks of 16 bytes
|
|
gc_err = gcry_cipher_encrypt(raop_aes_ctx, pkt->payload, (pkt->payload_len / 16) * 16, NULL, 0);
|
|
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);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
packet_send(struct raop_session *rs, struct rtp_packet *pkt)
|
|
{
|
|
int ret;
|
|
|
|
if (!rs)
|
|
return -1;
|
|
|
|
ret = send(rs->server_fd, pkt->data, pkt->data_len, 0);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno));
|
|
|
|
// Can't free it right away, it would make the ->next in the calling
|
|
// master_session and session loops invalid
|
|
deferred_session_failure(rs);
|
|
return -1;
|
|
}
|
|
else if (ret != pkt->data_len)
|
|
{
|
|
DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for '%s'\n", ret, rs->devname);
|
|
return -1;
|
|
}
|
|
|
|
/* DPRINTF(E_DBG, L_RAOP, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n",
|
|
rs->master_session->rtp_session->seqnum,
|
|
rs->master_session->rtp_session->pos,
|
|
pkt->header[1],
|
|
rs->master_session->rtp_session->pktbuf_len
|
|
);
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
control_packet_send(struct raop_session *rs, struct rtp_packet *pkt)
|
|
{
|
|
socklen_t addrlen;
|
|
int ret;
|
|
|
|
switch (rs->family)
|
|
{
|
|
case AF_INET:
|
|
rs->naddr.sin.sin_port = htons(rs->control_port);
|
|
addrlen = sizeof(rs->naddr.sin);
|
|
break;
|
|
|
|
case AF_INET6:
|
|
rs->naddr.sin6.sin6_port = htons(rs->control_port);
|
|
addrlen = sizeof(rs->naddr.sin6);
|
|
break;
|
|
|
|
default:
|
|
DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->family);
|
|
return;
|
|
}
|
|
|
|
ret = sendto(rs->control_svc->fd, pkt->data, pkt->data_len, 0, &rs->naddr.sa, addrlen);
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send playback sync to device '%s': %s\n", rs->devname, strerror(errno));
|
|
}
|
|
|
|
static void
|
|
packets_resend(struct raop_session *rs, uint16_t seqnum, int len)
|
|
{
|
|
struct rtp_session *rtp_session;
|
|
struct rtp_packet *pkt;
|
|
uint16_t s;
|
|
int i;
|
|
bool pkt_missing = false;
|
|
|
|
rtp_session = rs->master_session->rtp_session;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "Got retransmit request from '%s': seqnum %" PRIu16 " (len %d), last RTP session seqnum %" PRIu16 " (len %zu)\n",
|
|
rs->devname, seqnum, len, rtp_session->seqnum - 1, rtp_session->pktbuf_len);
|
|
|
|
// Note that seqnum may wrap around, so we don't use it for counting
|
|
for (i = 0, s = seqnum; i < len; i++, s++)
|
|
{
|
|
pkt = rtp_packet_get(rtp_session, s);
|
|
if (pkt)
|
|
packet_send(rs, pkt);
|
|
else
|
|
pkt_missing = true;
|
|
}
|
|
|
|
if (pkt_missing)
|
|
DPRINTF(E_WARN, L_RAOP, "Device '%s' retransmit request for seqnum %" PRIu16 " (len %d) is outside buffer range (last seqnum %" PRIu16 ", len %zu)\n",
|
|
rs->devname, seqnum, len, rtp_session->seqnum - 1, rtp_session->pktbuf_len);
|
|
}
|
|
|
|
static int
|
|
packets_send(struct raop_master_session *rms)
|
|
{
|
|
struct rtp_packet *pkt;
|
|
struct raop_session *rs;
|
|
int ret;
|
|
|
|
pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet, RAOP_RTP_PAYLOADTYPE, 0);
|
|
|
|
ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt);
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
for (rs = raop_sessions; rs; rs = rs->next)
|
|
{
|
|
if (rs->master_session != rms)
|
|
continue;
|
|
|
|
// Device just joined
|
|
if (rs->state == RAOP_STATE_CONNECTED)
|
|
{
|
|
pkt->header[1] = 0xe0;
|
|
packet_send(rs, pkt);
|
|
}
|
|
else if (rs->state == RAOP_STATE_STREAMING)
|
|
{
|
|
pkt->header[1] = 0x60;
|
|
packet_send(rs, pkt);
|
|
}
|
|
}
|
|
|
|
// Commits packet to retransmit buffer, and prepares the session for the next packet
|
|
rtp_packet_commit(rms->rtp_session, pkt);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Overview of rtptimes as they should be when starting a stream, and assuming
|
|
// the first rtptime (pos) is 88200:
|
|
// sync pkt: cur_pos = 0, rtptime = 88200
|
|
// audio pkt: rtptime = 88200
|
|
// RECORD: rtptime = 88200
|
|
// SET_PARAMETER text/artwork:
|
|
// rtptime = 88200
|
|
// SET_PARAMETER progress:
|
|
// progress = 72840/~88200/[len]
|
|
static inline void
|
|
timestamp_set(struct raop_master_session *rms, struct timespec ts)
|
|
{
|
|
// The last write from the player had a timestamp which has been passed to
|
|
// this function as ts. This is the player clock, which is more precise than
|
|
// the actual clock because it gives us a calculated time reference, which is
|
|
// independent of how busy the thread is. We save that here, we need this for
|
|
// reference when sending sync packets and progress.
|
|
rms->cur_stamp.ts = ts;
|
|
|
|
// So what rtptime should be playing, i.e. coming out of the speaker, at time
|
|
// ts (which is normally "now")? Let's calculate by example:
|
|
// - we started playback with a rtptime (pos) of X
|
|
// - up until time ts we have received a 1000 samples from the player
|
|
// - rms->output_buffer_samples is configured to 400 samples
|
|
// -> we should be playing rtptime X + 600
|
|
//
|
|
// So how do we measure samples received from player? We know that from the
|
|
// pos, which says how much has been sent to the device, and from rms->evbuf,
|
|
// which is the unsent stuff being buffered:
|
|
// - received = (pos - X) + rms->evbuf_samples
|
|
//
|
|
// This means the rtptime is computed as:
|
|
// - rtptime = X + received - rms->output_buffer_samples
|
|
// -> rtptime = X + (pos - X) + rms->evbuf_samples - rms->out_buffer_samples
|
|
// -> rtptime = pos + rms->evbuf_samples - rms->output_buffer_samples
|
|
rms->cur_stamp.pos = rms->rtp_session->pos + rms->evbuf_samples - rms->output_buffer_samples;
|
|
}
|
|
|
|
static void
|
|
packets_sync_send(struct raop_master_session *rms)
|
|
{
|
|
struct rtp_packet *sync_pkt;
|
|
struct raop_session *rs;
|
|
struct timespec ts;
|
|
bool is_sync_time;
|
|
|
|
// Check if it is time send a sync packet to sessions that are already running
|
|
is_sync_time = rtp_sync_is_time(rms->rtp_session);
|
|
|
|
// Just used for logging, the clock shouldn't be too far from rms->cur_stamp.ts
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
|
|
for (rs = raop_sessions; rs; rs = rs->next)
|
|
{
|
|
if (rs->master_session != rms)
|
|
continue;
|
|
|
|
// A device has joined and should get an init sync packet
|
|
if (rs->state == RAOP_STATE_CONNECTED)
|
|
{
|
|
sync_pkt = rtp_sync_packet_next(rms->rtp_session, rms->cur_stamp, 0x90);
|
|
control_packet_send(rs, sync_pkt);
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "Start sync packet sent to '%s': cur_pos=%" PRIu32 ", cur_ts=%ld.%09ld, clock=%ld.%09ld, rtptime=%" PRIu32 "\n",
|
|
rs->devname, rms->cur_stamp.pos, rms->cur_stamp.ts.tv_sec, rms->cur_stamp.ts.tv_nsec, ts.tv_sec, ts.tv_nsec, rms->rtp_session->pos);
|
|
}
|
|
else if (is_sync_time && rs->state == RAOP_STATE_STREAMING)
|
|
{
|
|
sync_pkt = rtp_sync_packet_next(rms->rtp_session, rms->cur_stamp, 0x80);
|
|
control_packet_send(rs, sync_pkt);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* ------------------------- Time and control service ----------------------- */
|
|
|
|
static void
|
|
service_stop(struct raop_service *svc)
|
|
{
|
|
if (svc->ev)
|
|
event_free(svc->ev);
|
|
|
|
if (svc->fd >= 0)
|
|
close(svc->fd);
|
|
|
|
svc->ev = NULL;
|
|
svc->fd = -1;
|
|
svc->port = 0;
|
|
}
|
|
|
|
static int
|
|
service_start(struct raop_service *svc, event_callback_fn cb, unsigned short port, const char *log_service_name)
|
|
{
|
|
memset(svc, 0, sizeof(struct raop_service));
|
|
|
|
svc->fd = net_bind(&port, SOCK_DGRAM, log_service_name);
|
|
if (svc->fd < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not start '%s' service\n", log_service_name);
|
|
goto error;
|
|
}
|
|
|
|
svc->ev = event_new(evbase_player, svc->fd, EV_READ | EV_PERSIST, cb, svc);
|
|
if (!svc->ev)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not create event for '%s' service\n", log_service_name);
|
|
goto error;
|
|
}
|
|
|
|
event_add(svc->ev, NULL);
|
|
|
|
svc->port = port;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
service_stop(svc);
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
timing_svc_cb(int fd, short what, void *arg)
|
|
{
|
|
struct raop_service *svc;
|
|
union net_sockaddr peer_addr;
|
|
socklen_t peer_addrlen = sizeof(peer_addr);
|
|
uint8_t req[32];
|
|
uint8_t res[32];
|
|
struct ntp_stamp recv_stamp;
|
|
struct ntp_stamp xmit_stamp;
|
|
int ret;
|
|
|
|
svc = (struct raop_service *)arg;
|
|
|
|
ret = timing_get_clock_ntp(&recv_stamp);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Couldn't get receive timestamp\n");
|
|
return;
|
|
}
|
|
|
|
peer_addrlen = sizeof(peer_addr);
|
|
ret = recvfrom(svc->fd, req, sizeof(req), 0, &peer_addr.sa, &peer_addrlen);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Error reading timing request: %s\n", strerror(errno));
|
|
return;
|
|
}
|
|
|
|
if (ret != 32)
|
|
{
|
|
DPRINTF(E_WARN, L_RAOP, "Got timing request with size %d\n", ret);
|
|
return;
|
|
}
|
|
|
|
if ((req[0] != 0x80) || (req[1] != 0xd2))
|
|
{
|
|
DPRINTF(E_WARN, L_RAOP, "Packet header doesn't match timing request (got 0x%02x%02x, expected 0x80d2)\n", req[0], req[1]);
|
|
return;
|
|
}
|
|
|
|
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 = 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, &peer_addr.sa, peer_addrlen);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send timing reply: %s\n", strerror(errno));
|
|
return;
|
|
}
|
|
}
|
|
|
|
static void
|
|
control_svc_cb(int fd, short what, void *arg)
|
|
{
|
|
struct raop_service *svc;
|
|
union net_sockaddr peer_addr = { 0 };
|
|
socklen_t peer_addrlen = sizeof(peer_addr);
|
|
char address[INET6_ADDRSTRLEN];
|
|
struct raop_session *rs;
|
|
uint8_t req[8];
|
|
uint16_t seq_start;
|
|
uint16_t seq_len;
|
|
int ret;
|
|
|
|
svc = (struct raop_service *)arg;
|
|
|
|
ret = recvfrom(svc->fd, req, sizeof(req), 0, &peer_addr.sa, &peer_addrlen);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Error reading control request: %s\n", strerror(errno));
|
|
return;
|
|
}
|
|
|
|
if (ret != 8)
|
|
{
|
|
DPRINTF(E_WARN, L_RAOP, "Got control request with size %d\n", ret);
|
|
return;
|
|
}
|
|
|
|
if ((req[0] != 0x80) || (req[1] != 0xd5))
|
|
{
|
|
DPRINTF(E_WARN, L_RAOP, "Packet header doesn't match retransmit request (got 0x%02x%02x, expected 0x80d5)\n", req[0], req[1]);
|
|
return;
|
|
}
|
|
|
|
rs = session_find_by_address(&peer_addr);
|
|
if (!rs)
|
|
{
|
|
net_address_get(address, sizeof(address), &peer_addr);
|
|
DPRINTF(E_WARN, L_RAOP, "Control request from %s; not a RAOP client\n", address);
|
|
return;
|
|
}
|
|
|
|
memcpy(&seq_start, req + 4, 2);
|
|
memcpy(&seq_len, req + 6, 2);
|
|
|
|
seq_start = be16toh(seq_start);
|
|
seq_len = be16toh(seq_len);
|
|
|
|
packets_resend(rs, seq_start, seq_len);
|
|
}
|
|
|
|
|
|
/* ------------------------------ Session startup --------------------------- */
|
|
|
|
static void
|
|
raop_cb_startup_retry(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
struct output_device *device;
|
|
int callback_id = rs->callback_id;
|
|
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
{
|
|
session_failure(rs);
|
|
return;
|
|
}
|
|
|
|
session_cleanup(rs);
|
|
raop_device_start(device, callback_id);
|
|
}
|
|
|
|
static void
|
|
raop_cb_startup_cancel(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_startup_cancel(struct raop_session *rs)
|
|
{
|
|
struct output_device *device;
|
|
int ret;
|
|
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device || !rs->session)
|
|
{
|
|
session_failure(rs);
|
|
return;
|
|
}
|
|
|
|
// Some devices don't seem to work with ipv6, so if the error wasn't a hard
|
|
// failure (bad password) we fall back to ipv4 and flag device as bad for ipv6
|
|
if (rs->family == AF_INET6 && !(rs->state & RAOP_STATE_F_FAILED))
|
|
{
|
|
// This flag is permanent and will not be overwritten by mdns advertisements
|
|
device->v6_disabled = 1;
|
|
|
|
// Stop current session and wait for call back
|
|
ret = raop_send_req_teardown(rs, raop_cb_startup_retry, "startup_cancel");
|
|
if (ret < 0)
|
|
raop_cb_startup_retry(NULL, rs); // No connection at all, call retry directly
|
|
|
|
return;
|
|
}
|
|
|
|
rs->state = RAOP_STATE_TEARDOWN;
|
|
|
|
ret = raop_send_req_teardown(rs, raop_cb_startup_cancel, "startup_cancel");
|
|
if (ret < 0)
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_cb_pin_start(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
goto error;
|
|
|
|
if (req->response_code != RTSP_OK)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Request for starting PIN verification 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_PASSWORD;
|
|
|
|
error:
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_cb_startup_volume(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
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;
|
|
|
|
ret = raop_metadata_startup_send(rs);
|
|
if (ret < 0)
|
|
goto cleanup;
|
|
|
|
rs->server_fd = net_connect(rs->address, rs->server_port, SOCK_DGRAM, "RAOP data");
|
|
if (rs->server_fd < 0)
|
|
goto cleanup;
|
|
|
|
rs->state = RAOP_STATE_CONNECTED;
|
|
|
|
/* 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 = arg;
|
|
const char *param;
|
|
int ret;
|
|
|
|
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 = arg;
|
|
const char *param;
|
|
char *transport;
|
|
char *token;
|
|
char *ptr;
|
|
int tmp;
|
|
int ret;
|
|
|
|
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_SPAM, 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))
|
|
{
|
|
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, "startup_setup");
|
|
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 = arg;
|
|
int ret;
|
|
|
|
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, "startup_announce");
|
|
if (ret < 0)
|
|
goto cleanup;
|
|
|
|
return;
|
|
|
|
cleanup:
|
|
raop_startup_cancel(rs);
|
|
}
|
|
|
|
static void
|
|
raop_cb_startup_auth_setup(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
goto cleanup;
|
|
|
|
if (req->response_code != RTSP_OK)
|
|
DPRINTF(E_WARN, L_RAOP, "Unexpected reply to auth-setup from '%s', proceeding anyway (%d %s)\n", rs->devname, req->response_code, req->response_code_line);
|
|
|
|
// Send ANNOUNCE
|
|
ret = raop_send_req_announce(rs, raop_cb_startup_announce, "startup_auth_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 = arg;
|
|
struct output_device *device;
|
|
const char *param;
|
|
int ret;
|
|
|
|
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);
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED) && (req->response_code != RTSP_FORBIDDEN))
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed '%s' (%s): %d %s\n", rs->devname, rs->address, 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' (%s)\n", rs->devname, rs->address);
|
|
|
|
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, "startup_options");
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not re-run OPTIONS request with authentication for '%s' (%s)\n", rs->devname, rs->address);
|
|
goto cleanup;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (req->response_code == RTSP_FORBIDDEN)
|
|
{
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
goto cleanup;
|
|
|
|
device->requires_auth = 1;
|
|
|
|
ret = raop_send_req_pin_start(rs, raop_cb_pin_start, "startup_options");
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not request PIN from '%s' (%s) for device verification\n", rs->devname, rs->address);
|
|
goto cleanup;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
rs->state = RAOP_STATE_OPTIONS;
|
|
|
|
param = evrtsp_find_header(req->input_headers, "Public");
|
|
if (param)
|
|
rs->supports_post = (strstr(param, "POST") != NULL);
|
|
else
|
|
DPRINTF(E_DBG, L_RAOP, "Could not find 'Public' header in OPTIONS reply from '%s' (%s)\n", rs->devname, rs->address);
|
|
|
|
if (rs->only_probe)
|
|
{
|
|
// Device probed successfully, tell our user
|
|
raop_status(rs);
|
|
|
|
// We're not going further with this session
|
|
session_cleanup(rs);
|
|
}
|
|
else if (rs->supports_post && rs->supports_auth_setup)
|
|
{
|
|
// AirPlay 2 devices require this step or the ANNOUNCE will get a 403
|
|
ret = raop_send_req_auth_setup(rs, raop_cb_startup_auth_setup, "startup_options");
|
|
if (ret < 0)
|
|
goto cleanup;
|
|
}
|
|
else
|
|
{
|
|
// Send ANNOUNCE
|
|
ret = raop_send_req_announce(rs, raop_cb_startup_announce, "startup_options");
|
|
if (ret < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
return;
|
|
|
|
cleanup:
|
|
if (rs->only_probe)
|
|
session_failure(rs);
|
|
else
|
|
raop_startup_cancel(rs);
|
|
}
|
|
|
|
|
|
/* ------------------------- tvOS device verification ----------------------- */
|
|
/* e.g. for the ATV4 (read it from the bottom and up) */
|
|
|
|
static int
|
|
raop_pair_response_process(int step, struct evrtsp_request *req, struct raop_session *rs)
|
|
{
|
|
uint8_t *response;
|
|
const char *errmsg;
|
|
size_t len;
|
|
int ret;
|
|
|
|
rs->reqs_in_flight--;
|
|
|
|
if (!req)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Verification step %d to '%s' failed, empty callback\n", step, rs->devname);
|
|
return -1;
|
|
}
|
|
|
|
if (req->response_code != RTSP_OK)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Verification step %d to '%s' failed with error code %d: %s\n", step, rs->devname, req->response_code, req->response_code_line);
|
|
return -1;
|
|
}
|
|
|
|
response = evbuffer_pullup(req->input_buffer, -1);
|
|
len = evbuffer_get_length(req->input_buffer);
|
|
|
|
switch (step)
|
|
{
|
|
case 1:
|
|
ret = pair_setup_response1(rs->pair_setup_ctx, response, len);
|
|
errmsg = pair_setup_errmsg(rs->pair_setup_ctx);
|
|
break;
|
|
case 2:
|
|
ret = pair_setup_response2(rs->pair_setup_ctx, response, len);
|
|
errmsg = pair_setup_errmsg(rs->pair_setup_ctx);
|
|
break;
|
|
case 3:
|
|
ret = pair_setup_response3(rs->pair_setup_ctx, response, len);
|
|
errmsg = pair_setup_errmsg(rs->pair_setup_ctx);
|
|
break;
|
|
case 4:
|
|
ret = pair_verify_response1(rs->pair_verify_ctx, response, len);
|
|
errmsg = pair_verify_errmsg(rs->pair_verify_ctx);
|
|
break;
|
|
case 5:
|
|
ret = 0;
|
|
break;
|
|
default:
|
|
ret = -1;
|
|
errmsg = "Bug! Bad step number";
|
|
}
|
|
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_RAOP, "Verification step %d response from '%s' error: %s\n", step, rs->devname, errmsg);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
raop_pair_request_send(int step, struct raop_session *rs, void (*cb)(struct evrtsp_request *, void *))
|
|
{
|
|
struct evrtsp_request *req;
|
|
uint8_t *body;
|
|
size_t len;
|
|
const char *errmsg;
|
|
const char *url;
|
|
const char *ctype;
|
|
int ret;
|
|
|
|
switch (step)
|
|
{
|
|
case 1:
|
|
body = pair_setup_request1(&len, rs->pair_setup_ctx);
|
|
errmsg = pair_setup_errmsg(rs->pair_setup_ctx);
|
|
url = "/pair-setup-pin";
|
|
ctype = "application/x-apple-binary-plist";
|
|
break;
|
|
case 2:
|
|
body = pair_setup_request2(&len, rs->pair_setup_ctx);
|
|
errmsg = pair_setup_errmsg(rs->pair_setup_ctx);
|
|
url = "/pair-setup-pin";
|
|
ctype = "application/x-apple-binary-plist";
|
|
break;
|
|
case 3:
|
|
body = pair_setup_request3(&len, rs->pair_setup_ctx);
|
|
errmsg = pair_setup_errmsg(rs->pair_setup_ctx);
|
|
url = "/pair-setup-pin";
|
|
ctype = "application/x-apple-binary-plist";
|
|
break;
|
|
case 4:
|
|
body = pair_verify_request1(&len, rs->pair_verify_ctx);
|
|
errmsg = pair_verify_errmsg(rs->pair_verify_ctx);
|
|
url = "/pair-verify";
|
|
ctype = "application/octet-stream";
|
|
break;
|
|
case 5:
|
|
body = pair_verify_request2(&len, rs->pair_verify_ctx);
|
|
errmsg = pair_verify_errmsg(rs->pair_verify_ctx);
|
|
url = "/pair-verify";
|
|
ctype = "application/octet-stream";
|
|
break;
|
|
default:
|
|
body = NULL;
|
|
errmsg = "Bug! Bad step number";
|
|
}
|
|
|
|
if (!body)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Verification step %d request error: %s\n", step, errmsg);
|
|
return -1;
|
|
}
|
|
|
|
req = evrtsp_request_new(cb, rs);
|
|
if (!req)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for verification step %d\n", step);
|
|
return -1;
|
|
}
|
|
|
|
evbuffer_add(req->output_buffer, body, len);
|
|
free(body);
|
|
|
|
ret = raop_add_headers(rs, req, EVRTSP_REQ_POST);
|
|
if (ret < 0)
|
|
{
|
|
evrtsp_request_free(req);
|
|
return -1;
|
|
}
|
|
|
|
evrtsp_add_header(req->output_headers, "Content-Type", ctype);
|
|
|
|
DPRINTF(E_INFO, L_RAOP, "Making verification request step %d to '%s'\n", step, rs->devname);
|
|
|
|
ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_POST, url);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Verification request step %d to '%s' failed\n", step, rs->devname);
|
|
return -1;
|
|
}
|
|
|
|
rs->reqs_in_flight++;
|
|
|
|
evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
raop_cb_pair_verify_step2(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
struct output_device *device;
|
|
int ret;
|
|
|
|
pair_verify_free(rs->pair_verify_ctx);
|
|
|
|
ret = raop_pair_response_process(5, req, rs);
|
|
if (ret < 0)
|
|
{
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
goto error;
|
|
|
|
// Clear auth_key, the device did not accept it
|
|
free(device->auth_key);
|
|
device->auth_key = NULL;
|
|
goto error;
|
|
}
|
|
|
|
DPRINTF(E_INFO, L_RAOP, "Verification of '%s' completed succesfully\n", rs->devname);
|
|
|
|
rs->state = RAOP_STATE_STARTUP;
|
|
|
|
raop_send_req_options(rs, raop_cb_startup_options, "verify_step2");
|
|
|
|
return;
|
|
|
|
error:
|
|
rs->state = RAOP_STATE_PASSWORD;
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_cb_pair_verify_step1(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
struct output_device *device;
|
|
int ret;
|
|
|
|
ret = raop_pair_response_process(4, req, rs);
|
|
if (ret < 0)
|
|
{
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
goto error;
|
|
|
|
// Clear auth_key, the device did not accept it
|
|
free(device->auth_key);
|
|
device->auth_key = NULL;
|
|
goto error;
|
|
}
|
|
|
|
ret = raop_pair_request_send(5, rs, raop_cb_pair_verify_step2);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
return;
|
|
|
|
error:
|
|
pair_verify_free(rs->pair_verify_ctx);
|
|
rs->pair_verify_ctx = NULL;
|
|
|
|
rs->state = RAOP_STATE_PASSWORD;
|
|
session_failure(rs);
|
|
}
|
|
|
|
static int
|
|
raop_pair_verify(struct raop_session *rs)
|
|
{
|
|
struct output_device *device;
|
|
int ret;
|
|
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
goto error;
|
|
|
|
CHECK_NULL(L_RAOP, rs->pair_verify_ctx = pair_verify_new(PAIR_CLIENT_FRUIT, device->auth_key, NULL));
|
|
|
|
ret = raop_pair_request_send(4, rs, raop_cb_pair_verify_step1);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
pair_verify_free(rs->pair_verify_ctx);
|
|
rs->pair_verify_ctx = NULL;
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
raop_cb_pair_setup_step3(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
struct output_device *device;
|
|
const char *authorization_key;
|
|
int ret;
|
|
|
|
ret = raop_pair_response_process(3, req, rs);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = pair_setup_result(&authorization_key, NULL, NULL, rs->pair_setup_ctx);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Verification setup result error: %s\n", pair_setup_errmsg(rs->pair_setup_ctx));
|
|
goto out;
|
|
}
|
|
|
|
DPRINTF(E_LOG, L_RAOP, "Verification setup stage complete, saving authorization key\n");
|
|
|
|
device = outputs_device_get(rs->device_id);
|
|
if (!device)
|
|
goto out;
|
|
|
|
free(device->auth_key);
|
|
device->auth_key = strdup(authorization_key);
|
|
|
|
// A blocking db call... :-~
|
|
db_speaker_save(device);
|
|
|
|
// No longer RAOP_STATE_PASSWORD
|
|
rs->state = RAOP_STATE_STOPPED;
|
|
|
|
out:
|
|
pair_setup_free(rs->pair_setup_ctx);
|
|
rs->pair_setup_ctx = NULL;
|
|
|
|
// Callback to player with result
|
|
raop_status(rs);
|
|
|
|
// We are telling the player that the device is now stopped, so we don't need
|
|
// the session any more
|
|
session_cleanup(rs);
|
|
}
|
|
|
|
static void
|
|
raop_cb_pair_setup_step2(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
ret = raop_pair_response_process(2, req, rs);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
ret = raop_pair_request_send(3, rs, raop_cb_pair_setup_step3);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
return;
|
|
|
|
error:
|
|
pair_setup_free(rs->pair_setup_ctx);
|
|
rs->pair_setup_ctx = NULL;
|
|
session_failure(rs);
|
|
}
|
|
|
|
static void
|
|
raop_cb_pair_setup_step1(struct evrtsp_request *req, void *arg)
|
|
{
|
|
struct raop_session *rs = arg;
|
|
int ret;
|
|
|
|
ret = raop_pair_response_process(1, req, rs);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
ret = raop_pair_request_send(2, rs, raop_cb_pair_setup_step2);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
return;
|
|
|
|
error:
|
|
pair_setup_free(rs->pair_setup_ctx);
|
|
rs->pair_setup_ctx = NULL;
|
|
session_failure(rs);
|
|
}
|
|
|
|
static int
|
|
raop_pair_setup(struct raop_session *rs, const char *pin)
|
|
{
|
|
int ret;
|
|
|
|
rs->pair_setup_ctx = pair_setup_new(PAIR_CLIENT_FRUIT, pin, NULL);
|
|
if (!rs->pair_setup_ctx)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Out of memory for verification setup context\n");
|
|
return -1;
|
|
}
|
|
|
|
ret = raop_pair_request_send(1, rs, raop_cb_pair_setup_step1);
|
|
if (ret < 0)
|
|
goto error;
|
|
|
|
rs->state = RAOP_STATE_PASSWORD;
|
|
|
|
return 0;
|
|
|
|
error:
|
|
pair_setup_free(rs->pair_setup_ctx);
|
|
rs->pair_setup_ctx = NULL;
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
raop_device_authorize(struct output_device *device, const char *pin, int callback_id)
|
|
{
|
|
struct raop_session *rs;
|
|
int ret;
|
|
|
|
// Make a session so we can communicate with the device
|
|
rs = session_make(device, callback_id, true);
|
|
if (!rs)
|
|
return -1;
|
|
|
|
ret = raop_pair_setup(rs, pin);
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not send verification setup request to '%s' (address %s)\n", device->name, rs->address);
|
|
session_cleanup(rs);
|
|
return -1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
/* ------------------ RAOP devices discovery - mDNS callback ---------------- */
|
|
/* Thread: main (mdns) */
|
|
|
|
|
|
/* Examples of txt content:
|
|
* HomePod
|
|
["cn=0,1,2,3" "da=true" "et=0,3,5" "ft=0x4A7FCA00,0x56BD0" "sf=0x404" "md=0,1,2" "am=AudioAccessory1,1" "pk=1...f" "tp=UDP" "vn=65537" "vs=356.19" "ov=11.2.5" "vv=2"]
|
|
* 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"]
|
|
["vv=2" "ov=11.4.1" "vs=366.75.2" "vn=65537" "tp=UDP" "pk=c...8" "am=AppleTV5,3" "md=0,1,2" "sf=0x10244" "ft=0x5A7FFFF7,0x155FDE" "et=0,3,5" "da=true" "cn=0,1,2,3"]
|
|
* Apple TV 4k:
|
|
["vv=2" "ov=13.3" "vs=415.3" "vn=65537" "tp=UDP" "pk=1...9" "am=AppleTV6,2" "md=0,1,2" "sf=0x30644" "ft=0x4A7FFFF7,0x3C155FDE" "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 *devcfg;
|
|
cfg_opt_t *cfgopt;
|
|
const char *p;
|
|
const char *device_name;
|
|
char *password;
|
|
char *s;
|
|
char *token;
|
|
char *ptr;
|
|
uint64_t id;
|
|
uint64_t sf;
|
|
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;
|
|
}
|
|
|
|
device_name = strchr(name, '@');
|
|
if (!device_name)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Could not extract AirPlay device name ('%s')\n", name);
|
|
|
|
return;
|
|
}
|
|
device_name++;
|
|
|
|
DPRINTF(E_DBG, L_RAOP, "Event for AirPlay device '%s' (port %d, id %" PRIx64 ")\n", device_name, port, id);
|
|
|
|
devcfg = cfg_gettsec(cfg, "airplay", device_name);
|
|
if (devcfg && cfg_getbool(devcfg, "exclude"))
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Excluding AirPlay device '%s' as set in config\n", device_name);
|
|
|
|
return;
|
|
}
|
|
if (devcfg && cfg_getbool(devcfg, "permanent") && (port < 0))
|
|
{
|
|
DPRINTF(E_INFO, L_RAOP, "AirPlay device '%s' disappeared, but set as permanent in config\n", device_name);
|
|
|
|
return;
|
|
}
|
|
if (devcfg && cfg_getbool(devcfg, "raop_disable"))
|
|
{
|
|
DPRINTF(E_INFO, L_RAOP, "Disabling AirPlay 1 (RAOP) for device '%s' as set in config\n", device_name);
|
|
|
|
return;
|
|
}
|
|
if (devcfg && cfg_getstr(devcfg, "nickname"))
|
|
{
|
|
device_name = cfg_getstr(devcfg, "nickname");
|
|
}
|
|
|
|
CHECK_NULL(L_RAOP, rd = calloc(1, sizeof(struct output_device)));
|
|
CHECK_NULL(L_RAOP, re = calloc(1, sizeof(struct raop_extra)));
|
|
|
|
rd->id = id;
|
|
rd->name = strdup(device_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", device_name);
|
|
|
|
goto free_rd;
|
|
}
|
|
|
|
if (*p == '\0')
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': tp has no value\n", device_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", device_name, p);
|
|
|
|
goto free_rd;
|
|
}
|
|
|
|
// Password protection
|
|
password = NULL;
|
|
p = keyval_get(txt, "pw");
|
|
if (!p)
|
|
{
|
|
rd->has_password = 0;
|
|
}
|
|
else if (*p == '\0')
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "AirPlay '%s': pw has no value\n", device_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", device_name);
|
|
|
|
if (devcfg)
|
|
password = cfg_getstr(devcfg, "password");
|
|
|
|
if (!password)
|
|
DPRINTF(E_LOG, L_RAOP, "No password given in config for AirPlay device '%s'\n", device_name);
|
|
}
|
|
|
|
rd->password = password;
|
|
|
|
// Device verification
|
|
p = keyval_get(txt, "sf");
|
|
if (p && (safe_hextou64(p, &sf) == 0))
|
|
{
|
|
if (sf & (1 << 9))
|
|
rd->requires_auth = 1;
|
|
|
|
// Note: device_add() in player.c will get the auth key from the db if available
|
|
}
|
|
|
|
// Quality supported - note this is mostly WIP, since newer devices that support
|
|
// higher than 44100/16 don't seem to use the below fields (probably use sf instead)
|
|
p = keyval_get(txt, "sr");
|
|
if (!p || (safe_atoi32(p, &rd->quality.sample_rate) != 0))
|
|
rd->quality.sample_rate = RAOP_QUALITY_SAMPLE_RATE_DEFAULT;
|
|
|
|
p = keyval_get(txt, "ss");
|
|
if (!p || (safe_atoi32(p, &rd->quality.bits_per_sample) != 0))
|
|
rd->quality.bits_per_sample = RAOP_QUALITY_BITS_PER_SAMPLE_DEFAULT;
|
|
|
|
p = keyval_get(txt, "ch");
|
|
if (!p || (safe_atoi32(p, &rd->quality.channels) != 0))
|
|
rd->quality.channels = RAOP_QUALITY_CHANNELS_DEFAULT;
|
|
|
|
if (!quality_is_equal(&rd->quality, &raop_quality_default))
|
|
DPRINTF(E_LOG, L_RAOP, "Device '%s' requested non-default audio quality (%d/%d/%d)\n", rd->name, rd->quality.sample_rate, rd->quality.bits_per_sample, rd->quality.channels);
|
|
|
|
// Max volume
|
|
rd->max_volume = devcfg ? cfg_getint(devcfg, "max_volume") : RAOP_CONFIG_MAX_VOLUME;
|
|
if ((rd->max_volume < 1) || (rd->max_volume > RAOP_CONFIG_MAX_VOLUME))
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device '%s', using default instead\n", rd->max_volume, device_name);
|
|
rd->max_volume = RAOP_CONFIG_MAX_VOLUME;
|
|
}
|
|
|
|
// 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 (strncmp(p, "AudioAccessory", strlen("AudioAccessory")) == 0)
|
|
re->devtype = RAOP_DEV_HOMEPOD;
|
|
else if (*p == '\0')
|
|
DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s': am has no value\n", device_name);
|
|
|
|
// If the user didn't set any reconnect setting we enable for Apple TV and
|
|
// HomePods due to https://github.com/owntone/owntone-server/issues/734
|
|
cfgopt = devcfg ? cfg_getopt(devcfg, "reconnect") : NULL;
|
|
if (cfgopt && cfgopt->nvalues == 1)
|
|
rd->resurrect = cfg_opt_getnbool(cfgopt, 0);
|
|
else
|
|
rd->resurrect = (re->devtype == RAOP_DEV_APPLETV4) || (re->devtype == RAOP_DEV_HOMEPOD);
|
|
|
|
// Encrypt stream
|
|
p = keyval_get(txt, "ek");
|
|
if (p && (*p == '1'))
|
|
re->encrypt = 1;
|
|
|
|
// Metadata support
|
|
p = keyval_get(txt, "md");
|
|
if (p)
|
|
{
|
|
CHECK_NULL(L_RAOP, s = strdup(p));
|
|
token = strtok_r(s, ",", &ptr);
|
|
while (token)
|
|
{
|
|
if (strcmp(token, "0") == 0)
|
|
re->wanted_metadata |= RAOP_MD_WANTS_TEXT;
|
|
else if (strcmp(token, "1") == 0)
|
|
re->wanted_metadata |= RAOP_MD_WANTS_ARTWORK;
|
|
else if (strcmp(token, "2") == 0)
|
|
re->wanted_metadata |= RAOP_MD_WANTS_PROGRESS;
|
|
|
|
token = strtok_r(NULL, ",", &ptr);
|
|
}
|
|
free(s);
|
|
}
|
|
|
|
p = keyval_get(txt, "et");
|
|
if (p)
|
|
{
|
|
CHECK_NULL(L_RAOP, s = strdup(p));
|
|
token = strtok_r(s, ",", &ptr);
|
|
while (token)
|
|
{
|
|
// Value of 4 seems to indicate support (!= requirement) for auth-setup
|
|
if (strcmp(token, "4") == 0)
|
|
re->supports_auth_setup = 1;
|
|
|
|
token = strtok_r(NULL, ",", &ptr);
|
|
}
|
|
free(s);
|
|
}
|
|
|
|
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, verification: %u, encrypt: %u, authsetup: %u, metadata: %u, type %s, address %s:%d\n",
|
|
device_name, rd->has_password, rd->requires_auth, re->encrypt, re->supports_auth_setup, re->wanted_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, verification: %u, encrypt: %u, authsetup: %u, metadata: %u, type %s, address [%s]:%d\n",
|
|
device_name, rd->has_password, rd->requires_auth, re->encrypt, re->supports_auth_setup, re->wanted_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", device_name);
|
|
goto free_rd;
|
|
}
|
|
|
|
ret = player_device_add(rd);
|
|
if (ret < 0)
|
|
goto free_rd;
|
|
|
|
return;
|
|
|
|
free_rd:
|
|
outputs_device_free(rd);
|
|
}
|
|
|
|
|
|
/* ---------------------------- Module definitions -------------------------- */
|
|
/* Thread: player */
|
|
|
|
static int
|
|
raop_device_start_generic(struct output_device *device, int callback_id, bool only_probe)
|
|
{
|
|
struct raop_session *rs;
|
|
int ret;
|
|
|
|
/* Send an OPTIONS request to establish the connection. If device verification
|
|
* is required we start with that. After that, we can determine our local
|
|
* address and build our session URL for all subsequent requests.
|
|
*/
|
|
|
|
rs = session_make(device, callback_id, only_probe);
|
|
if (!rs)
|
|
return -1;
|
|
|
|
if (device->auth_key)
|
|
ret = raop_pair_verify(rs);
|
|
else if (device->requires_auth)
|
|
ret = raop_send_req_pin_start(rs, raop_cb_pin_start, "device_start");
|
|
else
|
|
ret = raop_send_req_options(rs, raop_cb_startup_options, "device_start");
|
|
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_WARN, L_RAOP, "Could not send verification or OPTIONS request to '%s' (address %s)\n", device->name, rs->address);
|
|
session_cleanup(rs);
|
|
return -1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
raop_device_probe(struct output_device *device, int callback_id)
|
|
{
|
|
return raop_device_start_generic(device, callback_id, 1);
|
|
}
|
|
|
|
static int
|
|
raop_device_start(struct output_device *device, int callback_id)
|
|
{
|
|
return raop_device_start_generic(device, callback_id, 0);
|
|
}
|
|
|
|
static int
|
|
raop_device_stop(struct output_device *device, int callback_id)
|
|
{
|
|
struct raop_session *rs = device->session;
|
|
|
|
rs->callback_id = callback_id;
|
|
|
|
session_teardown(rs, "device_stop");
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
raop_device_flush(struct output_device *device, int callback_id)
|
|
{
|
|
struct raop_session *rs = device->session;
|
|
int ret;
|
|
|
|
if (rs->state != RAOP_STATE_STREAMING)
|
|
return 0; // No-op, nothing to flush
|
|
|
|
ret = raop_send_req_flush(rs, raop_cb_flush, "flush");
|
|
if (ret < 0)
|
|
return -1;
|
|
|
|
rs->callback_id = callback_id;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
raop_device_cb_set(struct output_device *device, int callback_id)
|
|
{
|
|
struct raop_session *rs = device->session;
|
|
|
|
rs->callback_id = callback_id;
|
|
}
|
|
|
|
static void
|
|
raop_device_free_extra(struct output_device *device)
|
|
{
|
|
struct raop_extra *re = device->extra_device_info;
|
|
|
|
free(re);
|
|
}
|
|
|
|
static void
|
|
raop_write(struct output_buffer *obuf)
|
|
{
|
|
struct raop_master_session *rms;
|
|
struct raop_session *rs;
|
|
int i;
|
|
|
|
for (rms = raop_master_sessions; rms; rms = rms->next)
|
|
{
|
|
for (i = 0; obuf->data[i].buffer; i++)
|
|
{
|
|
if (!quality_is_equal(&obuf->data[i].quality, &rms->rtp_session->quality))
|
|
continue;
|
|
|
|
// Set rms->cur_stamp, which involves a calculation of which session
|
|
// rtptime corresponds to the pts we are given by the player.
|
|
timestamp_set(rms, obuf->pts);
|
|
|
|
// Sends sync packets to new sessions, and if it is sync time then also to old sessions
|
|
packets_sync_send(rms);
|
|
|
|
// TODO avoid this copy
|
|
evbuffer_add(rms->evbuf, obuf->data[i].buffer, obuf->data[i].bufsize);
|
|
rms->evbuf_samples += obuf->data[i].samples;
|
|
|
|
// Send as many packets as we have data for (one packet requires rawbuf_size bytes)
|
|
while (evbuffer_get_length(rms->evbuf) >= rms->rawbuf_size)
|
|
{
|
|
evbuffer_remove(rms->evbuf, rms->rawbuf, rms->rawbuf_size);
|
|
rms->evbuf_samples -= rms->samples_per_packet;
|
|
|
|
packets_send(rms);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for devices that have joined since last write (we have already sent them
|
|
// initialization sync and rtp packets via packets_sync_send and packets_send)
|
|
for (rs = raop_sessions; rs; rs = rs->next)
|
|
{
|
|
if (rs->state != RAOP_STATE_CONNECTED)
|
|
continue;
|
|
|
|
// Start sending progress to keep ATV's alive
|
|
if (!event_pending(keep_alive_timer, EV_TIMEOUT, NULL))
|
|
evtimer_add(keep_alive_timer, &keep_alive_tv);
|
|
|
|
rs->state = RAOP_STATE_STREAMING;
|
|
// Make a cb?
|
|
}
|
|
}
|
|
|
|
static int
|
|
raop_init(void)
|
|
{
|
|
char ebuf[64];
|
|
char *ptr;
|
|
gpg_error_t gc_err;
|
|
int timing_port;
|
|
int control_port;
|
|
int ret;
|
|
|
|
// 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';
|
|
|
|
CHECK_NULL(L_RAOP, keep_alive_timer = evtimer_new(evbase_player, raop_keep_alive_timer_cb, NULL));
|
|
|
|
timing_port = cfg_getint(cfg_getsec(cfg, "airplay_shared"), "timing_port");
|
|
ret = service_start(&raop_timing_svc, timing_svc_cb, timing_port, "RAOP timing");
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "AirPlay time synchronization failed to start\n");
|
|
|
|
goto out_free_timer;
|
|
}
|
|
|
|
control_port = cfg_getint(cfg_getsec(cfg, "airplay_shared"), "control_port");
|
|
ret = service_start(&raop_control_svc, control_svc_cb, control_port, "RAOP control");
|
|
if (ret < 0)
|
|
{
|
|
DPRINTF(E_LOG, L_RAOP, "AirPlay playback control failed to start\n");
|
|
|
|
goto out_stop_timing;
|
|
}
|
|
|
|
ret = mdns_browse("_raop._tcp", raop_device_cb, MDNS_CONNECTION_TEST);
|
|
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:
|
|
service_stop(&raop_control_svc);
|
|
out_stop_timing:
|
|
service_stop(&raop_timing_svc);
|
|
out_free_timer:
|
|
event_free(keep_alive_timer);
|
|
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;
|
|
|
|
service_stop(&raop_control_svc);
|
|
service_stop(&raop_timing_svc);
|
|
|
|
event_free(keep_alive_timer);
|
|
|
|
for (rs = raop_sessions; raop_sessions; rs = raop_sessions)
|
|
{
|
|
raop_sessions = rs->next;
|
|
|
|
session_free(rs);
|
|
}
|
|
|
|
gcry_cipher_close(raop_aes_ctx);
|
|
|
|
free(raop_aes_key_b64);
|
|
free(raop_aes_iv_b64);
|
|
}
|
|
|
|
struct output_definition output_raop =
|
|
{
|
|
.name = "AirPlay 1",
|
|
.type = OUTPUT_TYPE_RAOP,
|
|
#ifdef PREFER_AIRPLAY2
|
|
.priority = 2,
|
|
#else
|
|
.priority = 1,
|
|
#endif
|
|
.disabled = 0,
|
|
.init = raop_init,
|
|
.deinit = raop_deinit,
|
|
.device_start = raop_device_start,
|
|
.device_stop = raop_device_stop,
|
|
.device_flush = raop_device_flush,
|
|
.device_probe = raop_device_probe,
|
|
.device_cb_set = raop_device_cb_set,
|
|
.device_free_extra = raop_device_free_extra,
|
|
.device_volume_set = raop_set_volume_one,
|
|
.device_volume_to_pct = raop_volume_to_pct,
|
|
.write = raop_write,
|
|
.metadata_prepare = raop_metadata_prepare,
|
|
.metadata_send = raop_metadata_send,
|
|
.metadata_purge = raop_metadata_purge,
|
|
.device_authorize = raop_device_authorize,
|
|
};
|