/* * Copyright (C) 2010-2011 Julien BLACHE * * 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 * GPLv2+ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* TODO: * - Support RTSP authentication in all requests (only OPTIONS so far) */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) || defined(__GLIBC__) # include # include #elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) # include #endif #include #include #include #include "evrtsp/evrtsp.h" #include #include "conffile.h" #include "logger.h" #include "misc.h" #include "player.h" #include "db.h" #include "artwork.h" #include "dmap_common.h" #include "raop.h" #ifndef MIN # define MIN(a, b) ((a < b) ? a : b) #endif #define AIRTUNES_V2_HDR_LEN 12 #define ALAC_HDR_LEN 3 #define AIRTUNES_V2_PKT_LEN (AIRTUNES_V2_HDR_LEN + ALAC_HDR_LEN + STOB(AIRTUNES_V2_PACKET_SAMPLES)) #define AIRTUNES_V2_PKT_TAIL_LEN (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN - ((AIRTUNES_V2_PKT_LEN / 16) * 16)) #define AIRTUNES_V2_PKT_TAIL_OFF (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_PKT_TAIL_LEN) #define RETRANSMIT_BUFFER_SIZE 1000 #define RAOP_MD_DELAY_STARTUP 15360 #define RAOP_MD_DELAY_SWITCH (RAOP_MD_DELAY_STARTUP * 2) /* This is an arbitrary value which just needs to be kept in sync with the config */ #define RAOP_CONFIG_MAX_VOLUME 11 struct raop_v2_packet { uint8_t clear[AIRTUNES_V2_PKT_LEN]; uint8_t encrypted[AIRTUNES_V2_PKT_LEN]; uint16_t seqnum; struct raop_v2_packet *prev; struct raop_v2_packet *next; }; struct raop_session { struct evrtsp_connection *ctrl; enum raop_session_state state; unsigned req_has_auth:1; unsigned encrypt:1; unsigned auth_quirk_itunes:1; unsigned wants_metadata:1; int reqs_in_flight; int cseq; char *session; char session_url[128]; char *realm; char *nonce; const char *password; char *devname; char *address; int volume; uint64_t start_rtptime; /* Do not dereference - only passed to the status cb */ struct raop_device *dev; raop_status_cb status_cb; /* AirTunes v2 */ unsigned short server_port; unsigned short control_port; unsigned short timing_port; int server_fd; union sockaddr_all sa; struct raop_service *timing_svc; struct raop_service *control_svc; struct raop_session *next; }; struct raop_metadata { struct evbuffer *metadata; struct evbuffer *artwork; int artwork_fmt; /* Progress data */ uint64_t start; uint64_t end; struct raop_metadata *next; }; struct raop_service { int fd; unsigned short port; struct event ev; }; struct raop_deferred_eh { struct event ev; struct raop_session *session; }; typedef void (*evrtsp_req_cb)(struct evrtsp_request *req, void *arg); /* Truncate RTP time to lower 32bits for RAOP */ #define RAOP_RTPTIME(x) ((uint32_t)((x) & (uint64_t)0xffffffff)) /* NTP timestamp definitions */ #define FRAC 4294967296. /* 2^32 as a double */ #define NTP_EPOCH_DELTA 0x83aa7e80 /* 2208988800 - that's 1970 - 1900 in seconds */ struct ntp_stamp { uint32_t sec; uint32_t frac; }; static const uint8_t raop_rsa_pubkey[] = "\xe7\xd7\x44\xf2\xa2\xe2\x78\x8b\x6c\x1f\x55\xa0\x8e\xb7\x05\x44" "\xa8\xfa\x79\x45\xaa\x8b\xe6\xc6\x2c\xe5\xf5\x1c\xbd\xd4\xdc\x68" "\x42\xfe\x3d\x10\x83\xdd\x2e\xde\xc1\xbf\xd4\x25\x2d\xc0\x2e\x6f" "\x39\x8b\xdf\x0e\x61\x48\xea\x84\x85\x5e\x2e\x44\x2d\xa6\xd6\x26" "\x64\xf6\x74\xa1\xf3\x04\x92\x9a\xde\x4f\x68\x93\xef\x2d\xf6\xe7" "\x11\xa8\xc7\x7a\x0d\x91\xc9\xd9\x80\x82\x2e\x50\xd1\x29\x22\xaf" "\xea\x40\xea\x9f\x0e\x14\xc0\xf7\x69\x38\xc5\xf3\x88\x2f\xc0\x32" "\x3d\xd9\xfe\x55\x15\x5f\x51\xbb\x59\x21\xc2\x01\x62\x9f\xd7\x33" "\x52\xd5\xe2\xef\xaa\xbf\x9b\xa0\x48\xd7\xb8\x13\xa2\xb6\x76\x7f" "\x6c\x3c\xcf\x1e\xb4\xce\x67\x3d\x03\x7b\x0d\x2e\xa3\x0c\x5f\xff" "\xeb\x06\xf8\xd0\x8a\xdd\xe4\x09\x57\x1a\x9c\x68\x9f\xef\x10\x72" "\x88\x55\xdd\x8c\xfb\x9a\x8b\xef\x5c\x89\x43\xef\x3b\x5f\xaa\x15" "\xdd\xe6\x98\xbe\xdd\xf3\x59\x96\x03\xeb\x3e\x6f\x61\x37\x2b\xb6" "\x28\xf6\x55\x9f\x59\x9a\x78\xbf\x50\x06\x87\xaa\x7f\x49\x76\xc0" "\x56\x2d\x41\x29\x56\xf8\x98\x9e\x18\xa6\x35\x5b\xd8\x15\x97\x82" "\x5e\x0f\xc8\x75\x34\x3e\xc7\x82\x11\x76\x25\xcd\xbf\x98\x44\x7b"; static const uint8_t raop_rsa_exp[] = "\x01\x00\x01"; /* From player.c */ extern struct event_base *evbase_player; /* RAOP AES stream key */ static uint8_t raop_aes_key[16]; static uint8_t raop_aes_iv[16]; static gcry_cipher_hd_t raop_aes_ctx; /* Base64-encoded AES key and IV for SDP */ static char *raop_aes_key_b64; static char *raop_aes_iv_b64; /* AirTunes v2 time synchronization */ static struct raop_service timing_4svc; static struct raop_service timing_6svc; /* AirTunes v2 playback synchronization / control */ static struct raop_service control_4svc; static struct raop_service control_6svc; static int sync_counter; /* AirTunes v2 audio stream */ static uint32_t ssrc_id; static uint16_t stream_seq; /* Retransmit packet buffer */ static int pktbuf_size; static struct raop_v2_packet *pktbuf_head; static struct raop_v2_packet *pktbuf_tail; /* Metadata */ static struct raop_metadata *metadata_head; static struct raop_metadata *metadata_tail; /* FLUSH timer */ static struct event flush_timer; /* Sessions */ static struct raop_session *sessions; /* ALAC bits writer - big endian * p outgoing buffer pointer * val bitfield value * blen bitfield length, max 8 bits * bpos bit position in the current byte (pointed by *p) */ static inline void alac_write_bits(uint8_t **p, uint8_t val, int blen, int *bpos) { int lb; int rb; int bd; /* Remaining bits in the current byte */ lb = 7 - *bpos + 1; /* Number of bits overflowing */ rb = lb - blen; if (rb >= 0) { bd = val << rb; if (*bpos == 0) **p = bd; else **p |= bd; /* No over- nor underflow, we're done with this byte */ if (rb == 0) { *p += 1; *bpos = 0; } else *bpos += blen; } else { /* Fill current byte */ bd = val >> -rb; **p |= bd; /* Overflow goes to the next byte */ *p += 1; **p = val << (8 + rb); *bpos = -rb; } } /* Raw data must be little endian */ static void alac_encode(uint8_t *raw, uint8_t *buf, int buflen) { uint8_t *maxraw; int bpos; bpos = 0; maxraw = raw + buflen; alac_write_bits(&buf, 1, 3, &bpos); /* channel=1, stereo */ alac_write_bits(&buf, 0, 4, &bpos); /* unknown */ alac_write_bits(&buf, 0, 8, &bpos); /* unknown */ alac_write_bits(&buf, 0, 4, &bpos); /* unknown */ alac_write_bits(&buf, 0, 1, &bpos); /* hassize */ alac_write_bits(&buf, 0, 2, &bpos); /* unused */ alac_write_bits(&buf, 1, 1, &bpos); /* is-not-compressed */ for (; raw < maxraw; raw += 4) { /* Byteswap to big endian */ alac_write_bits(&buf, *(raw + 1), 8, &bpos); alac_write_bits(&buf, *raw, 8, &bpos); alac_write_bits(&buf, *(raw + 3), 8, &bpos); alac_write_bits(&buf, *(raw + 2), 8, &bpos); } } /* AirTunes v2 time synchronization helpers */ static inline void timespec_to_ntp(struct timespec *ts, struct ntp_stamp *ns) { /* Seconds since NTP Epoch (1900-01-01) */ ns->sec = ts->tv_sec + NTP_EPOCH_DELTA; ns->frac = (uint32_t)((double)ts->tv_nsec * 1e-9 * FRAC); } static inline void ntp_to_timespec(struct ntp_stamp *ns, struct timespec *ts) { /* Seconds since Unix Epoch (1970-01-01) */ ts->tv_sec = ns->sec - NTP_EPOCH_DELTA; ts->tv_nsec = (long)((double)ns->frac / (1e-9 * FRAC)); } static inline int raop_v2_timing_get_clock_ntp(struct ntp_stamp *ns) { struct timespec ts; int ret; ret = clock_gettime(CLOCK_MONOTONIC, &ts); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get clock: %s\n", strerror(errno)); return -1; } timespec_to_ntp(&ts, ns); return 0; } /* RAOP crypto stuff - from VLC */ /* MGF1 is specified in RFC2437, section 10.2.1. Variables are named after the * specification. */ static int raop_crypt_mgf1(uint8_t *mask, size_t l, const uint8_t *z, const size_t zlen, const int hash) { char ebuf[64]; gcry_md_hd_t md_hdl; gpg_error_t gc_err; uint8_t *md; uint32_t counter; uint8_t c[4]; size_t copylen; int len; gc_err = gcry_md_open(&md_hdl, hash, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not open hash: %s\n", ebuf); return -1; } len = gcry_md_get_algo_dlen(hash); counter = 0; while (l > 0) { /* 3. For counter from 0 to \lceil{l / len}\rceil-1, do the following: * a. Convert counter to an octet string C of length 4 with the * primitive I2OSP: C = I2OSP (counter, 4) */ c[0] = (counter >> 24) & 0xff; c[1] = (counter >> 16) & 0xff; c[2] = (counter >> 8) & 0xff; c[3] = counter & 0xff; ++counter; /* b. Concatenate the hash of the seed z and c to the octet string T: * T = T || Hash (Z || C) */ gcry_md_reset(md_hdl); gcry_md_write(md_hdl, z, zlen); gcry_md_write(md_hdl, c, 4); md = gcry_md_read(md_hdl, hash); /* 4. Output the leading l octets of T as the octet string mask. */ copylen = MIN(l, len); memcpy(mask, md, copylen); mask += copylen; l -= copylen; } gcry_md_close(md_hdl); return 0; } /* EME-OAEP-ENCODE is specified in RFC2437, section 9.1.1.1. Variables are * named after the specification. */ static int raop_crypt_add_oaep_padding(uint8_t *em, const size_t emlen, const uint8_t *m, const size_t mlen, const uint8_t *p, const size_t plen) { uint8_t *seed; uint8_t *db; uint8_t *db_mask; uint8_t *seed_mask; size_t emlen_max; size_t pslen; size_t i; int hlen; int ret; /* Space for 0x00 prefix in EM. */ emlen_max = emlen - 1; hlen = gcry_md_get_algo_dlen(GCRY_MD_SHA1); /* Step 2: * If ||M|| > emLen-2hLen-1 then output "message too long" and stop. */ if (mlen > (emlen_max - (2 * hlen) - 1)) { DPRINTF(E_LOG, L_RAOP, "Could not add OAEP padding: message too long\n"); return -1; } /* Step 3: * Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero * octets. The length of PS may be 0. */ pslen = emlen_max - mlen - (2 * hlen) - 1; /* * Step 5: * Concatenate pHash, PS, the message M, and other padding to form a data * block DB as: DB = pHash || PS || 01 || M */ db = calloc(1, hlen + pslen + 1 + mlen); db_mask = calloc(1, emlen_max - hlen); seed_mask = calloc(1, hlen); if (!db || !db_mask || !seed_mask) { DPRINTF(E_LOG, L_RAOP, "Could not allocate memory for OAEP padding\n"); if (db) free(db); if (db_mask) free(db_mask); if (seed_mask) free(seed_mask); return -1; } /* Step 4: * Let pHash = Hash(P), an octet string of length hLen. */ gcry_md_hash_buffer(GCRY_MD_SHA1, db, p, plen); /* Step 3: * Generate an octet string PS consisting of emLen-||M||-2hLen-1 zero * octets. The length of PS may be 0. */ memset(db + hlen, 0, pslen); /* Step 5: * Concatenate pHash, PS, the message M, and other padding to form a data * block DB as: DB = pHash || PS || 01 || M */ db[hlen + pslen] = 0x01; memcpy(db + hlen + pslen + 1, m, mlen); /* Step 6: * Generate a random octet string seed of length hLen */ seed = gcry_random_bytes(hlen, GCRY_STRONG_RANDOM); if (!seed) { DPRINTF(E_LOG, L_RAOP, "Could not allocate memory for OAEP seed\n"); ret = -1; goto out_free_alloced; } /* Step 7: * Let dbMask = MGF(seed, emLen-hLen). */ ret = raop_crypt_mgf1(db_mask, emlen_max - hlen, seed, hlen, GCRY_MD_SHA1); if (ret < 0) goto out_free_all; /* Step 8: * Let maskedDB = DB \xor dbMask. */ for (i = 0; i < (emlen_max - hlen); i++) db[i] ^= db_mask[i]; /* Step 9: * Let seedMask = MGF(maskedDB, hLen). */ ret = raop_crypt_mgf1(seed_mask, hlen, db, emlen_max - hlen, GCRY_MD_SHA1); if (ret < 0) goto out_free_all; /* Step 10: * Let maskedSeed = seed \xor seedMask. */ for (i = 0; i < hlen; i++) seed[i] ^= seed_mask[i]; /* Step 11: * Let EM = maskedSeed || maskedDB. */ em[0] = 0x00; memcpy(em + 1, seed, hlen); memcpy(em + 1 + hlen, db, hlen + pslen + 1 + mlen); /* Step 12: * Output EM. */ ret = 0; out_free_all: free(seed); out_free_alloced: free(db); free(db_mask); free(seed_mask); return ret; } static char * raop_crypt_encrypt_aes_key_base64(void) { char ebuf[64]; uint8_t padded_key[256]; gpg_error_t gc_err; gcry_sexp_t sexp_rsa_params; gcry_sexp_t sexp_input; gcry_sexp_t sexp_encrypted; gcry_sexp_t sexp_token_a; gcry_mpi_t mpi_pubkey; gcry_mpi_t mpi_exp; gcry_mpi_t mpi_input; gcry_mpi_t mpi_output; char *result; uint8_t *value; size_t value_size; int ret; result = NULL; /* Add RSA-OAES-SHA1 padding */ ret = raop_crypt_add_oaep_padding(padded_key, sizeof(padded_key), raop_aes_key, sizeof(raop_aes_key), NULL, 0); if (ret < 0) return NULL; /* Read public key */ gc_err = gcry_mpi_scan(&mpi_pubkey, GCRYMPI_FMT_USG, raop_rsa_pubkey, sizeof(raop_rsa_pubkey) - 1, NULL); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not read RAOP RSA pubkey: %s\n", ebuf); return NULL; } /* Read exponent */ gc_err = gcry_mpi_scan(&mpi_exp, GCRYMPI_FMT_USG, raop_rsa_exp, sizeof(raop_rsa_exp) - 1, NULL); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not read RAOP RSA exponent: %s\n", ebuf); goto out_free_mpi_pubkey; } /* If the input data starts with a set bit (0x80), gcrypt thinks it's a * signed integer and complains. Prefixing it with a zero byte (\0) * works, but involves more work. Converting it to an MPI in our code is * cleaner. */ gc_err = gcry_mpi_scan(&mpi_input, GCRYMPI_FMT_USG, padded_key, sizeof(padded_key), NULL); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not convert input data: %s\n", ebuf); goto out_free_mpi_exp; } /* Build S-expression with RSA parameters */ gc_err = gcry_sexp_build(&sexp_rsa_params, NULL, "(public-key(rsa(n %m)(e %m)))", mpi_pubkey, mpi_exp); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not build RSA params S-exp: %s\n", ebuf); goto out_free_mpi_input; } /* Build S-expression for data */ gc_err = gcry_sexp_build(&sexp_input, NULL, "(data(value %m))", mpi_input); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not build data S-exp: %s\n", ebuf); goto out_free_sexp_params; } /* Encrypt data */ gc_err = gcry_pk_encrypt(&sexp_encrypted, sexp_input, sexp_rsa_params); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not encrypt data: %s\n", ebuf); goto out_free_sexp_input; } /* Extract encrypted data */ sexp_token_a = gcry_sexp_find_token(sexp_encrypted, "a", 0); if (!sexp_token_a) { DPRINTF(E_LOG, L_RAOP, "Could not find token 'a' in result S-exp\n"); goto out_free_sexp_encrypted; } mpi_output = gcry_sexp_nth_mpi(sexp_token_a, 1, GCRYMPI_FMT_USG); if (!mpi_output) { DPRINTF(E_LOG, L_RAOP, "Cannot extract MPI from result\n"); goto out_free_sexp_token_a; } /* Copy encrypted data into char array */ gc_err = gcry_mpi_aprint(GCRYMPI_FMT_USG, &value, &value_size, mpi_output); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not copy encrypted data: %s\n", ebuf); goto out_free_mpi_output; } /* Encode in Base64 */ result = b64_encode(value, value_size); free(value); out_free_mpi_output: gcry_mpi_release(mpi_output); out_free_sexp_token_a: gcry_sexp_release(sexp_token_a); out_free_sexp_encrypted: gcry_sexp_release(sexp_encrypted); out_free_sexp_input: gcry_sexp_release(sexp_input); out_free_sexp_params: gcry_sexp_release(sexp_rsa_params); out_free_mpi_input: gcry_mpi_release(mpi_input); out_free_mpi_exp: gcry_mpi_release(mpi_exp); out_free_mpi_pubkey: gcry_mpi_release(mpi_pubkey); return result; } /* RAOP metadata */ static void raop_metadata_free(struct raop_metadata *rmd) { evbuffer_free(rmd->metadata); if (rmd->artwork) evbuffer_free(rmd->artwork); free(rmd); } void raop_metadata_purge(void) { struct raop_metadata *rmd; for (rmd = metadata_head; rmd; rmd = metadata_head) { metadata_head = rmd->next; raop_metadata_free(rmd); } metadata_tail = NULL; } void raop_metadata_prune(uint64_t rtptime) { struct raop_metadata *rmd; for (rmd = metadata_head; rmd; rmd = metadata_head) { if (rmd->end >= rtptime) break; if (metadata_tail == metadata_head) metadata_tail = rmd->next; metadata_head = rmd->next; raop_metadata_free(rmd); } } static struct raop_metadata * raop_metadata_prepare(int id, uint64_t rtptime) { struct query_params qp; struct db_media_file_info dbmfi; char filter[32]; struct raop_metadata *rmd; struct evbuffer *tmp; uint64_t duration; int ret; rmd = (struct raop_metadata *)malloc(sizeof(struct raop_metadata)); if (!rmd) { DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP metadata\n"); return NULL; } memset(rmd, 0, sizeof(struct raop_metadata)); /* Get dbmfi */ memset(&qp, 0, sizeof(struct query_params)); qp.type = Q_ITEMS; qp.idx_type = I_NONE; qp.sort = S_NONE; qp.filter = filter; ret = snprintf(filter, sizeof(filter), "id = %d", id); if ((ret < 0) || (ret >= sizeof(filter))) { DPRINTF(E_LOG, L_RAOP, "Could not build filter for file id %d; metadata will not be sent\n", id); goto out_rmd; } ret = db_query_start(&qp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't start query; no metadata will be sent\n"); goto out_rmd; } ret = db_query_fetch_file(&qp, &dbmfi); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't fetch file id %d; metadata will not be sent\n", id); goto out_query; } /* Turn it into DAAP metadata */ tmp = evbuffer_new(); if (!tmp) { DPRINTF(E_LOG, L_RAOP, "Out of memory for temporary metadata evbuffer; metadata will not be sent\n"); goto out_query; } rmd->metadata = evbuffer_new(); if (!rmd->metadata) { DPRINTF(E_LOG, L_RAOP, "Out of memory for metadata evbuffer; metadata will not be sent\n"); evbuffer_free(tmp); goto out_query; } ret = dmap_encode_file_metadata(rmd->metadata, tmp, &dbmfi, NULL, 0, 0, 1); evbuffer_free(tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n"); goto out_metadata; } /* Progress */ ret = safe_atou64(dbmfi.song_length, &duration); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Failed to convert song_length to integer; no metadata will be sent\n"); goto out_metadata; } rmd->start = rtptime; rmd->end = rtptime + (duration * 44100UL) / 1000UL; /* Get artwork */ rmd->artwork = evbuffer_new(); if (!rmd->artwork) { DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n"); goto skip_artwork; } ret = artwork_get_item_filename(dbmfi.path, 600, 600, ART_CAN_PNG | ART_CAN_JPEG, rmd->artwork); if (ret < 0) { DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for '%s' (%d); no artwork will be sent\n", dbmfi.title, id); evbuffer_free(rmd->artwork); rmd->artwork = NULL; } rmd->artwork_fmt = ret; skip_artwork: db_query_end(&qp); /* Add rmd to metadata list */ if (metadata_tail) metadata_tail->next = rmd; else { metadata_head = rmd; metadata_tail = rmd; } return rmd; out_metadata: evbuffer_free(rmd->metadata); out_query: db_query_end(&qp); out_rmd: free(rmd); return NULL; } /* Helpers */ static int raop_add_auth(struct raop_session *rs, struct evrtsp_request *req, const char *method, const char *uri) { char ha1[33]; char ha2[33]; char ebuf[64]; char auth[256]; const char *hash_fmt; const char *username; uint8_t *hash_bytes; size_t hashlen; gcry_md_hd_t hd; gpg_error_t gc_err; int i; int ret; rs->req_has_auth = 0; if (!rs->nonce) return 0; if (!rs->password) { DPRINTF(E_LOG, L_RAOP, "Authentication required but no password found for device %s\n", rs->devname); return -2; } if (rs->auth_quirk_itunes) { hash_fmt = "%02X"; /* Uppercase hex */ username = "iTunes"; } else { hash_fmt = "%02x"; username = ""; /* No username */ } gc_err = gcry_md_open(&hd, GCRY_MD_MD5, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not open MD5: %s\n", ebuf); return -1; } memset(ha1, 0, sizeof(ha1)); memset(ha2, 0, sizeof(ha2)); hashlen = gcry_md_get_algo_dlen(GCRY_MD_MD5); /* HA 1 */ gcry_md_write(hd, username, strlen(username)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, rs->realm, strlen(rs->realm)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, rs->password, strlen(rs->password)); hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n"); return -1; } for (i = 0; i < hashlen; i++) sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]); /* RESET */ gcry_md_reset(hd); /* HA 2 */ gcry_md_write(hd, method, strlen(method)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, uri, strlen(uri)); hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n"); return -1; } for (i = 0; i < hashlen; i++) sprintf(ha2 + (2 * i), hash_fmt, hash_bytes[i]); /* RESET */ gcry_md_reset(hd); /* Final value */ gcry_md_write(hd, ha1, 32); gcry_md_write(hd, ":", 1); gcry_md_write(hd, rs->nonce, strlen(rs->nonce)); gcry_md_write(hd, ":", 1); gcry_md_write(hd, ha2, 32); hash_bytes = gcry_md_read(hd, GCRY_MD_MD5); if (!hash_bytes) { DPRINTF(E_LOG, L_RAOP, "Could not read MD5 hash\n"); return -1; } for (i = 0; i < hashlen; i++) sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]); gcry_md_close(hd); /* Build header */ ret = snprintf(auth, sizeof(auth), "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"", username, rs->realm, rs->nonce, uri, ha1); if ((ret < 0) || (ret >= sizeof(auth))) { DPRINTF(E_LOG, L_RAOP, "Authorization value header exceeds buffer size\n"); return -1; } evrtsp_add_header(req->output_headers, "Authorization", auth); DPRINTF(E_DBG, L_RAOP, "Authorization header: %s\n", auth); rs->req_has_auth = 1; return 0; } static int raop_parse_auth(struct raop_session *rs, struct evrtsp_request *req) { const char *param; char *auth; char *token; char *ptr; if (rs->realm) { free(rs->realm); rs->realm = NULL; } if (rs->nonce) { free(rs->nonce); rs->nonce = NULL; } param = evrtsp_find_header(req->input_headers, "WWW-Authenticate"); if (!param) { DPRINTF(E_LOG, L_RAOP, "WWW-Authenticate header not found\n"); return -1; } DPRINTF(E_DBG, L_RAOP, "WWW-Authenticate: %s\n", param); if (strncmp(param, "Digest ", strlen("Digest ")) != 0) { DPRINTF(E_LOG, L_RAOP, "Unsupported authentication method: %s\n", param); return -1; } auth = strdup(param); if (!auth) { DPRINTF(E_LOG, L_RAOP, "Out of memory for WWW-Authenticate header copy\n"); return -1; } token = strchr(auth, ' '); token++; token = strtok_r(token, " =", &ptr); while (token) { if (strcmp(token, "realm") == 0) { token = strtok_r(NULL, "=\"", &ptr); if (!token) break; rs->realm = strdup(token); } else if (strcmp(token, "nonce") == 0) { token = strtok_r(NULL, "=\"", &ptr); if (!token) break; rs->nonce = strdup(token); } token = strtok_r(NULL, " =", &ptr); } free(auth); if (!rs->realm || !rs->nonce) { DPRINTF(E_LOG, L_RAOP, "Could not find realm/nonce in WWW-Authenticate header\n"); if (rs->realm) { free(rs->realm); rs->realm = NULL; } if (rs->nonce) { free(rs->nonce); rs->nonce = NULL; } return -1; } DPRINTF(E_DBG, L_RAOP, "Found realm: [%s], nonce: [%s]\n", rs->realm, rs->nonce); return 0; } static int raop_add_headers(struct raop_session *rs, struct evrtsp_request *req, enum evrtsp_cmd_type req_method) { char buf[64]; const char *method; const char *url; int ret; method = evrtsp_method(req_method); DPRINTF(E_DBG, L_RAOP, "Building %s for %s\n", method, rs->devname); snprintf(buf, sizeof(buf), "%d", rs->cseq); evrtsp_add_header(req->output_headers, "CSeq", buf); rs->cseq++; evrtsp_add_header(req->output_headers, "User-Agent", "forked-daapd/" VERSION); /* Add Authorization header */ url = (req_method == EVRTSP_REQ_OPTIONS) ? "*" : rs->session_url; ret = raop_add_auth(rs, req, method, url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not add Authorization header\n"); if (ret == -2) rs->state = RAOP_PASSWORD; return -1; } snprintf(buf, sizeof(buf), "%" PRIX64, libhash); evrtsp_add_header(req->output_headers, "Client-Instance", buf); evrtsp_add_header(req->output_headers, "DACP-ID", buf); if (rs->session) evrtsp_add_header(req->output_headers, "Session", rs->session); /* Content-Length added automatically by evrtsp */ return 0; } static int raop_grab_cseq(struct evkeyvalq *headers) { const char *param; int cseq; int ret; param = evrtsp_find_header(headers, "CSeq"); if (!param) return -1; ret = safe_atoi32(param, &cseq); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not convert CSeq value to integer (%s)\n", param); return -1; } return cseq; } static int raop_check_cseq(struct raop_session *rs, struct evrtsp_request *req) { int reply_cseq; int request_cseq; reply_cseq = raop_grab_cseq(req->input_headers); /* AirFoil won't return cseq, so skip the check */ if (reply_cseq < 0) { DPRINTF(E_INFO, L_RAOP, "No CSeq in reply, skipping check\n"); return 0; } request_cseq = raop_grab_cseq(req->output_headers); if (request_cseq < 0) { DPRINTF(E_LOG, L_RAOP, "No CSeq in request\n"); return -1; } if (reply_cseq == request_cseq) return 0; DPRINTF(E_LOG, L_RAOP, "Reply CSeq does not match request CSeq: got %d expected %d\n", reply_cseq, request_cseq); return -1; } static int raop_make_sdp(struct raop_session *rs, struct evrtsp_request *req, char *address, uint32_t session_id) { #define SDP_PLD_FMT \ "v=0\r\n" \ "o=iTunes %u 0 IN IP4 %s\r\n" \ "s=iTunes\r\n" \ "c=IN IP4 %s\r\n" \ "t=0 0\r\n" \ "m=audio 0 RTP/AVP 96\r\n" \ "a=rtpmap:96 AppleLossless\r\n" \ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n" \ "a=rsaaeskey:%s\r\n" \ "a=aesiv:%s\r\n" #define SDP_PLD_FMT_NO_ENC \ "v=0\r\n" \ "o=iTunes %u 0 IN IP4 %s\r\n" \ "s=iTunes\r\n" \ "c=IN IP4 %s\r\n" \ "t=0 0\r\n" \ "m=audio 0 RTP/AVP 96\r\n" \ "a=rtpmap:96 AppleLossless\r\n" \ "a=fmtp:96 %d 0 16 40 10 14 2 255 0 0 44100\r\n" char *p; int ret; p = strchr(rs->address, '%'); if (p) *p = '\0'; /* Add SDP payload - but don't add RSA/AES key/iv if no encryption - important for ATV3 update 6.0 */ if (rs->encrypt) ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT, session_id, address, rs->address, AIRTUNES_V2_PACKET_SAMPLES, raop_aes_key_b64, raop_aes_iv_b64); else ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT_NO_ENC, session_id, address, rs->address, AIRTUNES_V2_PACKET_SAMPLES); if (p) *p = '%'; if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Out of memory for SDP payload\n"); return -1; } return 0; #undef SDP_PLD_FMT #undef SDP_PLD_FMT_NO_ENC } /* RAOP/RTSP requests */ /* * Request queueing HOWTO * * Sending: * - increment rs->reqs_in_flight * - set evrtsp connection closecb to NULL * * Request callback: * - decrement rs->reqs_in_flight first thing, even if the callback is * called for error handling (req == NULL or HTTP error code) * - if rs->reqs_in_flight == 0, setup evrtsp connection closecb * * When a request fails, the whole RAOP session is declared failed and * torn down by calling raop_session_failure(), even if there are requests * queued on the evrtsp connection. There is no reason to think pending * requests would work out better than the one that just failed and recovery * would be tricky to get right. * * evrtsp behaviour with queued requests: * - request callback is called with req == NULL to indicate a connection * error; if there are several requests queued on the connection, this can * happen for each request if the connection isn't destroyed * - the connection is reset, and the closecb is called if the connection was * previously connected. There is no closecb set when there are requests in * flight */ static int raop_send_req_teardown(struct raop_session *rs, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for TEARDOWN\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_TEARDOWN); if (ret < 0) { evrtsp_request_free(req); return -1; } ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_TEARDOWN, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make TEARDOWN request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static int raop_send_req_flush(struct raop_session *rs, uint64_t rtptime, evrtsp_req_cb cb) { char buf[64]; struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for FLUSH\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_FLUSH); if (ret < 0) { evrtsp_request_free(req); return -1; } /* Restart sequence: last sequence + 1 */ ret = snprintf(buf, sizeof(buf), "seq=%u;rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rtptime)); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in FLUSH request\n"); evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "RTP-Info", buf); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_FLUSH, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make FLUSH request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static int raop_send_req_set_parameter(struct raop_session *rs, struct evbuffer *evbuf, char *ctype, char *rtpinfo, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for SET_PARAMETER\n"); return -1; } ret = evbuffer_add_buffer(req->output_buffer, evbuf); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload\n"); evrtsp_request_free(req); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_SET_PARAMETER); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Content-Type", ctype); if (rtpinfo) evrtsp_add_header(req->output_headers, "RTP-Info", rtpinfo); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_SET_PARAMETER, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make SET_PARAMETER request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static int raop_send_req_record(struct raop_session *rs, evrtsp_req_cb cb) { char buf[64]; struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for RECORD\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_RECORD); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Range", "npt=0-"); /* Start sequence: next sequence */ ret = snprintf(buf, sizeof(buf), "seq=%u;rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rs->start_rtptime)); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in RECORD request\n"); evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "RTP-Info", buf); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_RECORD, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make RECORD request\n"); return -1; } rs->reqs_in_flight++; return 0; } static int raop_send_req_setup(struct raop_session *rs, evrtsp_req_cb cb) { char hdr[128]; struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for SETUP\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_SETUP); if (ret < 0) { evrtsp_request_free(req); return -1; } /* Request UDP transport, AirTunes v2 streaming */ ret = snprintf(hdr, sizeof(hdr), "RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;control_port=%u;timing_port=%u", rs->control_svc->port, rs->timing_svc->port); if ((ret < 0) || (ret >= sizeof(hdr))) { DPRINTF(E_LOG, L_RAOP, "Transport header exceeds buffer length\n"); evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Transport", hdr); ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_SETUP, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make SETUP request\n"); return -1; } rs->reqs_in_flight++; return 0; } static int raop_send_req_announce(struct raop_session *rs, evrtsp_req_cb cb) { uint8_t challenge[16]; char *challenge_b64; char *ptr; struct evrtsp_request *req; char *address; char *intf; unsigned short port; uint32_t session_id; int ret; /* Determine local address, needed for SDP and session URL */ evrtsp_connection_get_local_address(rs->ctrl, &address, &port); if (!address || (port == 0)) { DPRINTF(E_LOG, L_RAOP, "Could not determine local address\n"); if (address) free(address); return -1; } intf = strchr(address, '%'); if (intf) { *intf = '\0'; intf++; } DPRINTF(E_DBG, L_RAOP, "Local address: %s (LL: %s) port %d\n", address, (intf) ? intf : "no", port); req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for ANNOUNCE\n"); free(address); return -1; } /* Session ID and session URL */ gcry_randomize(&session_id, sizeof(session_id), GCRY_STRONG_RANDOM); ret = snprintf(rs->session_url, sizeof(rs->session_url), "rtsp://%s/%u", address, session_id); if ((ret < 0) || (ret >= sizeof(rs->session_url))) { DPRINTF(E_LOG, L_RAOP, "Session URL length exceeds 127 characters\n"); free(address); goto cleanup_req; } /* SDP payload */ ret = raop_make_sdp(rs, req, address, session_id); free(address); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not generate SDP payload for ANNOUNCE\n"); goto cleanup_req; } ret = raop_add_headers(rs, req, EVRTSP_REQ_ANNOUNCE); if (ret < 0) { evrtsp_request_free(req); return -1; } evrtsp_add_header(req->output_headers, "Content-Type", "application/sdp"); /* Challenge - but only if session is encrypted (important for ATV3 after update 6.0) */ if (rs->encrypt) { gcry_randomize(challenge, sizeof(challenge), GCRY_STRONG_RANDOM); challenge_b64 = b64_encode(challenge, sizeof(challenge)); if (!challenge_b64) { DPRINTF(E_LOG, L_RAOP, "Couldn't encode challenge\n"); goto cleanup_req; } /* Remove base64 padding */ ptr = strchr(challenge_b64, '='); if (ptr) *ptr = '\0'; evrtsp_add_header(req->output_headers, "Apple-Challenge", challenge_b64); free(challenge_b64); } ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_ANNOUNCE, rs->session_url); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make ANNOUNCE request\n"); return -1; } rs->reqs_in_flight++; return 0; cleanup_req: evrtsp_request_free(req); return -1; } static int raop_send_req_options(struct raop_session *rs, evrtsp_req_cb cb) { struct evrtsp_request *req; int ret; req = evrtsp_request_new(cb, rs); if (!req) { DPRINTF(E_LOG, L_RAOP, "Could not create RTSP request for OPTIONS\n"); return -1; } ret = raop_add_headers(rs, req, EVRTSP_REQ_OPTIONS); if (ret < 0) { evrtsp_request_free(req); return -1; } ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_OPTIONS, "*"); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not make OPTIONS request\n"); return -1; } rs->reqs_in_flight++; evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); return 0; } static void raop_session_free(struct raop_session *rs) { evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); evrtsp_connection_free(rs->ctrl); close(rs->server_fd); if (rs->realm) free(rs->realm); if (rs->nonce) free(rs->nonce); if (rs->session) free(rs->session); if (rs->address) free(rs->address); if (rs->devname) free(rs->devname); free(rs); } static void raop_session_cleanup(struct raop_session *rs) { struct raop_session *s; struct raop_v2_packet *pkt; struct raop_v2_packet *next_pkt; if (rs == sessions) sessions = sessions->next; else { for (s = sessions; s && (s->next != rs); s = s->next) ; /* EMPTY */ if (!s) DPRINTF(E_WARN, L_RAOP, "WARNING: struct raop_session not found in list; BUG!\n"); else s->next = rs->next; } raop_session_free(rs); /* No more active sessions, free retransmit buffer */ if (!sessions) { pkt = pktbuf_head; while (pkt) { next_pkt = pkt->next; free(pkt); pkt = next_pkt; } pktbuf_head = NULL; pktbuf_tail = NULL; pktbuf_size = 0; } } static void raop_session_failure(struct raop_session *rs) { /* Session failed, let our user know */ if (rs->state != RAOP_PASSWORD) rs->state = RAOP_FAILED; rs->status_cb(rs->dev, rs, rs->state); raop_session_cleanup(rs); } static void raop_deferred_eh_cb(int fd, short what, void *arg) { struct raop_deferred_eh *deh; struct raop_session *rs; deh = (struct raop_deferred_eh *)arg; rs = deh->session; free(deh); DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device %s\n", rs->devname); raop_session_failure(rs); } static void raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg) { struct timeval tv; struct raop_deferred_eh *deh; struct raop_session *rs; rs = (struct raop_session *)arg; DPRINTF(E_LOG, L_RAOP, "ApEx %s closed RTSP connection\n", rs->devname); rs->state = RAOP_FAILED; deh = (struct raop_deferred_eh *)malloc(sizeof(struct raop_deferred_eh)); if (!deh) { DPRINTF(E_LOG, L_RAOP, "Out of memory for deferred error handling!\n"); return; } deh->session = rs; evtimer_set(&deh->ev, raop_deferred_eh_cb, deh); event_base_set(evbase_player, &deh->ev); evutil_timerclear(&tv); evtimer_add(&deh->ev, &tv); } static struct raop_session * raop_session_make(struct raop_device *rd, int family, raop_status_cb cb) { struct raop_session *rs; char *address; char *intf; unsigned short port; int ret; switch (family) { case AF_INET: /* We always have the v4 services, so no need to check */ if (!rd->v4_address) return NULL; address = rd->v4_address; port = rd->v4_port; break; case AF_INET6: if (!rd->v6_address || (timing_6svc.fd < 0) || (control_6svc.fd < 0)) return NULL; address = rd->v6_address; port = rd->v6_port; break; default: return NULL; } rs = (struct raop_session *)malloc(sizeof(struct raop_session)); if (!rs) { DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP session\n"); return NULL; } memset(rs, 0, sizeof(struct raop_session)); rs->state = RAOP_STOPPED; rs->reqs_in_flight = 0; rs->cseq = 1; rs->dev = rd; rs->status_cb = cb; rs->server_fd = -1; rs->password = rd->password; rs->wants_metadata = rd->wants_metadata; switch (rd->devtype) { case RAOP_DEV_APEX1_80211G: rs->encrypt = 1; rs->auth_quirk_itunes = 1; break; case RAOP_DEV_APEX2_80211N: rs->encrypt = 1; rs->auth_quirk_itunes = 0; break; case RAOP_DEV_APEX3_80211N: rs->encrypt = 0; rs->auth_quirk_itunes = 0; break; case RAOP_DEV_APPLETV: rs->encrypt = 0; rs->auth_quirk_itunes = 0; break; case RAOP_DEV_OTHER: rs->encrypt = rd->encrypt; rs->auth_quirk_itunes = 0; break; } rs->ctrl = evrtsp_connection_new(address, port); if (!rs->ctrl) { DPRINTF(E_LOG, L_RAOP, "Could not create control connection to %s\n", address); goto out_free_rs; } evrtsp_connection_set_base(rs->ctrl, evbase_player); rs->sa.ss.ss_family = family; switch (family) { case AF_INET: rs->timing_svc = &timing_4svc; rs->control_svc = &control_4svc; ret = inet_pton(AF_INET, address, &rs->sa.sin.sin_addr); break; case AF_INET6: rs->timing_svc = &timing_6svc; rs->control_svc = &control_6svc; intf = strchr(address, '%'); if (intf) *intf = '\0'; ret = inet_pton(AF_INET6, address, &rs->sa.sin6.sin6_addr); if (intf) { *intf = '%'; intf++; rs->sa.sin6.sin6_scope_id = if_nametoindex(intf); if (rs->sa.sin6.sin6_scope_id == 0) { DPRINTF(E_LOG, L_RAOP, "Could not find interface %s\n", intf); ret = -1; break; } } break; default: ret = -1; break; } if (ret <= 0) { DPRINTF(E_LOG, L_RAOP, "Device address not valid (%s)\n", address); goto out_free_evcon; } rs->devname = strdup(rd->name); rs->address = strdup(address); rs->volume = rd->volume; rs->next = sessions; sessions = rs; return rs; out_free_evcon: evrtsp_connection_free(rs->ctrl); out_free_rs: free(rs); return NULL; } static void raop_session_failure_cb(struct evrtsp_request *req, void *arg) { struct raop_session *rs; rs = (struct raop_session *)arg; raop_session_failure(rs); } /* Metadata handling */ static void raop_cb_metadata(struct evrtsp_request *req, void *arg) { struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for metadata/artwork/progress: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; /* No status_cb call, user doesn't want/need to know about the status * of metadata requests unless they cause the session to fail. */ if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } static int raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) { uint32_t display; int ret; /* Here's the deal with progress values: * - first value, called display, is always start minus a delay * -> delay x1 if streaming is starting for this device (joining or not) * -> delay x2 if stream is switching to a new song * - second value, called start, is the RTP time of the first sample for this * song for this device * -> start of song * -> start of song + offset if device is joining in the middle of a song, * or getting out of a pause or seeking * - third value, called end, is the RTP time of the last sample for this song */ display = RAOP_RTPTIME(rmd->start - delay); ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, RAOP_RTPTIME(rmd->start + offset), RAOP_RTPTIME(rmd->end)); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not build progress string for sending\n"); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n"); return ret; } static int raop_metadata_send_artwork(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, char *rtptime) { char *ctype; int ret; switch (rmd->artwork_fmt) { case ART_FMT_PNG: ctype = "image/png"; break; case ART_FMT_JPEG: ctype = "image/jpeg"; break; default: DPRINTF(E_LOG, L_RAOP, "Unsupported artwork format %d\n", rmd->artwork_fmt); return -1; } ret = evbuffer_add(evbuf, EVBUFFER_DATA(rmd->artwork), EVBUFFER_LENGTH(rmd->artwork)); if (ret != 0) { DPRINTF(E_LOG, L_RAOP, "Could not copy artwork for sending\n"); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, ctype, rtptime, raop_cb_metadata); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n"); return ret; } static int raop_metadata_send_metadata(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, char *rtptime) { int ret; ret = evbuffer_add(evbuf, EVBUFFER_DATA(rmd->metadata), EVBUFFER_LENGTH(rmd->metadata)); if (ret != 0) { DPRINTF(E_LOG, L_RAOP, "Could not copy metadata for sending\n"); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, "application/x-dmap-tagged", rtptime, raop_cb_metadata); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for metadata\n"); return ret; } static int raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) { char rtptime[32]; struct evbuffer *evbuf; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_RAOP, "Could not allocate temp evbuffer for metadata processing\n"); return -1; } ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", RAOP_RTPTIME(rmd->start)); if ((ret < 0) || (ret >= sizeof(rtptime))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer while sending metadata\n"); ret = -1; goto out; } ret = raop_metadata_send_metadata(rs, evbuf, rmd, rtptime); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send metadata to %s\n", rs->devname); ret = -1; goto out; } if (!rmd->artwork) goto skip_artwork; ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send artwork to %s\n", rs->devname); ret = -1; goto out; } skip_artwork: ret = raop_metadata_send_progress(rs, evbuf, rmd, offset, delay); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send progress to %s\n", rs->devname); ret = -1; goto out; } out: evbuffer_free(evbuf); return ret; } static void raop_metadata_startup_send(struct raop_session *rs) { struct raop_metadata *rmd; uint64_t offset; int sent; int ret; if (!rs->wants_metadata) return; sent = 0; for (rmd = metadata_head; rmd; rmd = rmd->next) { /* Current song */ if ((rs->start_rtptime >= rmd->start) && (rs->start_rtptime < rmd->end)) { offset = rs->start_rtptime - rmd->start; ret = raop_metadata_send_internal(rs, rmd, offset, RAOP_MD_DELAY_STARTUP); if (ret < 0) { raop_session_failure(rs); return; } sent = 1; } /* Next song(s) */ else if (sent && (rs->start_rtptime < rmd->start)) { ret = raop_metadata_send_internal(rs, rmd, 0, RAOP_MD_DELAY_SWITCH); if (ret < 0) { raop_session_failure(rs); return; } } } } void raop_metadata_send(int id, uint64_t rtptime, uint64_t offset, int startup) { struct raop_session *rs; struct raop_metadata *rmd; uint32_t delay; int ret; rmd = raop_metadata_prepare(id, rtptime); if (!rmd) return; for (rs = sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_F_CONNECTED)) continue; if (!rs->wants_metadata) continue; delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; ret = raop_metadata_send_internal(rs, rmd, offset, delay); if (ret < 0) { raop_session_failure(rs); continue; } } } /* Volume handling */ static float raop_volume_convert(int volume, char *name) { float raop_volume; cfg_t *airplay; int max_volume; max_volume = RAOP_CONFIG_MAX_VOLUME; airplay = cfg_gettsec(cfg, "airplay", name); if (airplay) max_volume = cfg_getint(airplay, "max_volume"); if ((max_volume < 1) || (max_volume > RAOP_CONFIG_MAX_VOLUME)) { DPRINTF(E_LOG, L_RAOP, "Config has bad max_volume (%d) for device %s, using default instead\n", max_volume, name); max_volume = RAOP_CONFIG_MAX_VOLUME; } DPRINTF(E_DBG, L_RAOP, "Setting max_volume for device %s to %d\n", name, max_volume); /* RAOP volume * -144.0 is off * 0 - -30.0 maps to 100 - 0 */ if (volume == 0) raop_volume = -144.0; else raop_volume = -30.0 + ((float)max_volume * (float)volume * 30.0) / (100.0 * RAOP_CONFIG_MAX_VOLUME); return raop_volume; } static int raop_set_volume_internal(struct raop_session *rs, int volume, evrtsp_req_cb cb) { struct evbuffer *evbuf; float raop_volume; int ret; evbuf = evbuffer_new(); if (!evbuf) { DPRINTF(E_LOG, L_RAOP, "Could not allocate evbuffer for volume payload\n"); return -1; } raop_volume = raop_volume_convert(volume, rs->devname); /* Don't let locales get in the way here */ ret = evbuffer_add_printf(evbuf, "volume: %d.%06d\r\n", (int)raop_volume, -(int)(1000000.0 * (raop_volume - (int)raop_volume))); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Out of memory for SET_PARAMETER payload (volume)\n"); evbuffer_free(evbuf); return -1; } ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, cb); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER request for volume\n"); evbuffer_free(evbuf); return ret; } static void raop_cb_set_volume(struct evrtsp_request *req, void *arg) { raop_status_cb status_cb; struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for stream volume: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; /* Let our user know */ status_cb = rs->status_cb; rs->status_cb = NULL; status_cb(rs->dev, rs, rs->state); if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } /* Volume in [0 - 100] */ int raop_set_volume_one(struct raop_session *rs, int volume, raop_status_cb cb) { int ret; if (!(rs->state & RAOP_F_CONNECTED)) return 0; ret = raop_set_volume_internal(rs, volume, raop_cb_set_volume); if (ret < 0) { raop_session_failure(rs); return 0; } rs->status_cb = cb; return 1; } static void raop_cb_flush(struct evrtsp_request *req, void *arg) { raop_status_cb status_cb; struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "FLUSH request failed: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; rs->state = RAOP_CONNECTED; /* Let our user know */ status_cb = rs->status_cb; rs->status_cb = NULL; status_cb(rs->dev, rs, rs->state); if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; error: raop_session_failure(rs); } static void raop_flush_timer_cb(int fd, short what, void *arg) { struct raop_session *rs; DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP sessions\n"); for (rs = sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_F_CONNECTED)) continue; raop_device_stop(rs); } } int raop_flush(raop_status_cb cb, uint64_t rtptime) { struct timeval tv; struct raop_session *rs; int pending; int ret; pending = 0; for (rs = sessions; rs; rs = rs->next) { if (rs->state != RAOP_STREAMING) continue; ret = raop_send_req_flush(rs, rtptime, raop_cb_flush); if (ret < 0) { raop_session_failure(rs); continue; } rs->status_cb = cb; pending++; } if (pending > 0) { evtimer_set(&flush_timer, raop_flush_timer_cb, NULL); event_base_set(evbase_player, &flush_timer); evutil_timerclear(&tv); tv.tv_sec = 10; evtimer_add(&flush_timer, &tv); } return pending; } /* AirTunes v2 time synchronization */ static void raop_v2_timing_cb(int fd, short what, void *arg) { char address[INET6_ADDRSTRLEN]; union sockaddr_all sa; uint8_t req[32]; uint8_t res[32]; struct ntp_stamp recv_stamp; struct ntp_stamp xmit_stamp; struct raop_session *rs; struct raop_service *svc; int len; int ret; svc = (struct raop_service *)arg; ret = raop_v2_timing_get_clock_ntp(&recv_stamp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get receive timestamp\n"); goto readd; } len = sizeof(sa.ss); ret = recvfrom(svc->fd, req, sizeof(req), 0, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Error reading timing request: %s\n", strerror(errno)); goto readd; } if (ret != 32) { DPRINTF(E_DBG, L_RAOP, "Got timing request with size %d\n", ret); goto readd; } switch (sa.ss.ss_family) { case AF_INET: if (svc != &timing_4svc) goto readd; for (rs = sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET) && (sa.sin.sin_addr.s_addr == rs->sa.sin.sin_addr.s_addr)) break; } if (!rs) ret = (inet_ntop(AF_INET, &sa.sin.sin_addr.s_addr, address, sizeof(address)) != NULL); break; case AF_INET6: if (svc != &timing_6svc) goto readd; for (rs = sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET6) && IN6_ARE_ADDR_EQUAL(&sa.sin6.sin6_addr, &rs->sa.sin6.sin6_addr)) break; } if (!rs) ret = (inet_ntop(AF_INET6, &sa.sin6.sin6_addr.s6_addr, address, sizeof(address)) != NULL); break; default: DPRINTF(E_LOG, L_RAOP, "Time sync: Unknown address family %d\n", sa.ss.ss_family); goto readd; } if (!rs) { if (!ret) DPRINTF(E_LOG, L_RAOP, "Time sync request from [error: %s]; not a RAOP client\n", strerror(errno)); else DPRINTF(E_LOG, L_RAOP, "Time sync request from %s; not a RAOP client\n", address); goto readd; } if ((req[0] != 0x80) || (req[1] != 0xd2)) { DPRINTF(E_LOG, L_RAOP, "Packet header doesn't match timing request\n"); goto readd; } memset(res, 0, sizeof(res)); /* Header */ res[0] = 0x80; res[1] = 0xd3; res[2] = req[2]; /* Copy client timestamp */ memcpy(res + 8, req + 24, 8); /* Receive timestamp */ recv_stamp.sec = htobe32(recv_stamp.sec); recv_stamp.frac = htobe32(recv_stamp.frac); memcpy(res + 16, &recv_stamp.sec, 4); memcpy(res + 20, &recv_stamp.frac, 4); /* Transmit timestamp */ ret = raop_v2_timing_get_clock_ntp(&xmit_stamp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get transmit timestamp, falling back to receive timestamp\n"); /* Still better than failing altogether * recv/xmit are close enough that it shouldn't matter much */ memcpy(res + 24, &recv_stamp.sec, 4); memcpy(res + 28, &recv_stamp.frac, 4); } else { xmit_stamp.sec = htobe32(xmit_stamp.sec); xmit_stamp.frac = htobe32(xmit_stamp.frac); memcpy(res + 24, &xmit_stamp.sec, 4); memcpy(res + 28, &xmit_stamp.frac, 4); } ret = sendto(svc->fd, res, sizeof(res), 0, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send timing reply: %s\n", strerror(errno)); goto readd; } readd: ret = event_add(&svc->ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't re-add event for timing requests\n"); return; } } static int raop_v2_timing_start_one(struct raop_service *svc, int family) { union sockaddr_all sa; int on; int len; int ret; #ifdef __linux__ svc->fd = socket(family, SOCK_DGRAM | SOCK_CLOEXEC, 0); #else svc->fd = socket(family, SOCK_DGRAM, 0); #endif if (svc->fd < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't make timing socket: %s\n", strerror(errno)); return -1; } if (family == AF_INET6) { on = 1; ret = setsockopt(svc->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not set IPV6_V6ONLY on timing socket: %s\n", strerror(errno)); goto out_fail; } } memset(&sa, 0, sizeof(union sockaddr_all)); sa.ss.ss_family = family; switch (family) { case AF_INET: sa.sin.sin_addr.s_addr = INADDR_ANY; sa.sin.sin_port = 0; len = sizeof(sa.sin); break; case AF_INET6: sa.sin6.sin6_addr = in6addr_any; sa.sin6.sin6_port = 0; len = sizeof(sa.sin6); break; } ret = bind(svc->fd, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't bind timing socket: %s\n", strerror(errno)); goto out_fail; } len = sizeof(sa.ss); ret = getsockname(svc->fd, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get timing socket name: %s\n", strerror(errno)); goto out_fail; } switch (family) { case AF_INET: svc->port = ntohs(sa.sin.sin_port); DPRINTF(E_DBG, L_RAOP, "Timing IPv4 port: %d\n", svc->port); break; case AF_INET6: svc->port = ntohs(sa.sin6.sin6_port); DPRINTF(E_DBG, L_RAOP, "Timing IPv6 port: %d\n", svc->port); break; } event_set(&svc->ev, svc->fd, EV_READ, raop_v2_timing_cb, svc); event_base_set(evbase_player, &svc->ev); ret = event_add(&svc->ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't add event for timing requests\n"); goto out_fail; } return 0; out_fail: close(svc->fd); svc->fd = -1; svc->port = 0; return -1; } static void raop_v2_timing_stop(void) { if (event_initialized(&timing_4svc.ev)) event_del(&timing_4svc.ev); if (event_initialized(&timing_6svc.ev)) event_del(&timing_6svc.ev); close(timing_4svc.fd); timing_4svc.fd = -1; timing_4svc.port = 0; close(timing_6svc.fd); timing_6svc.fd = -1; timing_6svc.port = 0; } static int raop_v2_timing_start(int v6enabled) { int ret; if (v6enabled) { ret = raop_v2_timing_start_one(&timing_6svc, AF_INET6); if (ret < 0) DPRINTF(E_WARN, L_RAOP, "Could not start timing service on IPv6\n"); } ret = raop_v2_timing_start_one(&timing_4svc, AF_INET); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not start timing service on IPv4\n"); raop_v2_timing_stop(); return -1; } return 0; } /* AirTunes v2 playback synchronization */ static void raop_v2_control_send_sync(uint64_t next_pkt, struct timespec *init) { uint8_t msg[20]; struct timespec ts; struct ntp_stamp cur_stamp; struct raop_session *rs; uint64_t cur_pos; uint32_t cur_pos32; uint32_t next_pkt32; int len; int ret; memset(msg, 0, sizeof(msg)); msg[0] = (sync_counter == 0) ? 0x90 : 0x80; msg[1] = 0xd4; msg[3] = 0x07; next_pkt32 = htobe32(RAOP_RTPTIME(next_pkt)); memcpy(msg + 16, &next_pkt32, 4); if (!init) { ret = player_get_current_pos(&cur_pos, &ts, 1); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not get current playback position and clock\n"); return; } timespec_to_ntp(&ts, &cur_stamp); } else { cur_pos = next_pkt - 88200; timespec_to_ntp(init, &cur_stamp); } cur_pos32 = htobe32(RAOP_RTPTIME(cur_pos)); cur_stamp.sec = htobe32(cur_stamp.sec); cur_stamp.frac = htobe32(cur_stamp.frac); memcpy(msg + 4, &cur_pos32, 4); memcpy(msg + 8, &cur_stamp.sec, 4); memcpy(msg + 12, &cur_stamp.frac, 4); for (rs = sessions; rs; rs = rs->next) { if (rs->state != RAOP_STREAMING) continue; switch (rs->sa.ss.ss_family) { case AF_INET: rs->sa.sin.sin_port = htons(rs->control_port); len = sizeof(rs->sa.sin); break; case AF_INET6: rs->sa.sin6.sin6_port = htons(rs->control_port); len = sizeof(rs->sa.sin6); break; default: DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); continue; } ret = sendto(rs->control_svc->fd, msg, sizeof(msg), 0, &rs->sa.sa, len); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "Could not send playback sync to device %s: %s\n", rs->devname, strerror(errno)); } } /* Forward */ static void raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len); static void raop_v2_control_cb(int fd, short what, void *arg) { char address[INET6_ADDRSTRLEN]; union sockaddr_all sa; uint8_t req[8]; struct raop_session *rs; struct raop_service *svc; uint16_t seq_start; uint16_t seq_len; int len; int ret; svc = (struct raop_service *)arg; len = sizeof(sa.ss); ret = recvfrom(svc->fd, req, sizeof(req), 0, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Error reading control request: %s\n", strerror(errno)); goto readd; } if (ret != 8) { DPRINTF(E_DBG, L_RAOP, "Got control request with size %d\n", ret); goto readd; } switch (sa.ss.ss_family) { case AF_INET: if (svc != &control_4svc) goto readd; for (rs = sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET) && (sa.sin.sin_addr.s_addr == rs->sa.sin.sin_addr.s_addr)) break; } if (!rs) ret = (inet_ntop(AF_INET, &sa.sin.sin_addr.s_addr, address, sizeof(address)) != NULL); break; case AF_INET6: if (svc != &control_6svc) goto readd; for (rs = sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET6) && IN6_ARE_ADDR_EQUAL(&sa.sin6.sin6_addr, &rs->sa.sin6.sin6_addr)) break; } if (!rs) ret = (inet_ntop(AF_INET6, &sa.sin6.sin6_addr.s6_addr, address, sizeof(address)) != NULL); break; default: DPRINTF(E_LOG, L_RAOP, "Control svc: Unknown address family %d\n", sa.ss.ss_family); goto readd; } if (!rs) { if (!ret) DPRINTF(E_LOG, L_RAOP, "Control request from [error: %s]; not a RAOP client\n", strerror(errno)); else DPRINTF(E_LOG, L_RAOP, "Control request from %s; not a RAOP client\n", address); goto readd; } if ((req[0] != 0x80) || (req[1] != 0xd5)) { DPRINTF(E_LOG, L_RAOP, "Packet header doesn't match retransmit request\n"); goto readd; } memcpy(&seq_start, req + 4, 2); memcpy(&seq_len, req + 6, 2); seq_start = be16toh(seq_start); seq_len = be16toh(seq_len); DPRINTF(E_DBG, L_RAOP, "Got retransmit request, seq_start %u len %u\n", seq_start, seq_len); raop_v2_resend_range(rs, seq_start, seq_len); readd: ret = event_add(&svc->ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't re-add event for control requests\n"); return; } } static int raop_v2_control_start_one(struct raop_service *svc, int family) { union sockaddr_all sa; int on; int len; int ret; #ifdef __linux__ svc->fd = socket(family, SOCK_DGRAM | SOCK_CLOEXEC, 0); #else svc->fd = socket(family, SOCK_DGRAM, 0); #endif if (svc->fd < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't make control socket: %s\n", strerror(errno)); return -1; } if (family == AF_INET6) { on = 1; ret = setsockopt(svc->fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not set IPV6_V6ONLY on control socket: %s\n", strerror(errno)); goto out_fail; } } memset(&sa, 0, sizeof(union sockaddr_all)); sa.ss.ss_family = family; switch (family) { case AF_INET: sa.sin.sin_addr.s_addr = INADDR_ANY; sa.sin.sin_port = 0; len = sizeof(sa.sin); break; case AF_INET6: sa.sin6.sin6_addr = in6addr_any; sa.sin6.sin6_port = 0; len = sizeof(sa.sin6); break; } ret = bind(svc->fd, &sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't bind control socket: %s\n", strerror(errno)); goto out_fail; } len = sizeof(sa.ss); ret = getsockname(svc->fd, &sa.sa, (socklen_t *)&len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't get control socket name: %s\n", strerror(errno)); goto out_fail; } switch (family) { case AF_INET: svc->port = ntohs(sa.sin.sin_port); DPRINTF(E_DBG, L_RAOP, "Control IPv4 port: %d\n", svc->port); break; case AF_INET6: svc->port = ntohs(sa.sin6.sin6_port); DPRINTF(E_DBG, L_RAOP, "Control IPv6 port: %d\n", svc->port); break; } event_set(&svc->ev, svc->fd, EV_READ, raop_v2_control_cb, svc); event_base_set(evbase_player, &svc->ev); ret = event_add(&svc->ev, NULL); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Couldn't add event for control requests\n"); goto out_fail; } return 0; out_fail: close(svc->fd); svc->fd = -1; svc->port = 0; return -1; } static void raop_v2_control_stop(void) { if (event_initialized(&control_4svc.ev)) event_del(&control_4svc.ev); if (event_initialized(&control_6svc.ev)) event_del(&control_6svc.ev); close(control_4svc.fd); control_4svc.fd = -1; control_4svc.port = 0; close(control_6svc.fd); control_6svc.fd = -1; control_6svc.port = 0; } static int raop_v2_control_start(int v6enabled) { int ret; if (v6enabled) { ret = raop_v2_control_start_one(&control_6svc, AF_INET6); if (ret < 0) DPRINTF(E_WARN, L_RAOP, "Could not start control service on IPv6\n"); } ret = raop_v2_control_start_one(&control_4svc, AF_INET); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not start control service on IPv4\n"); raop_v2_control_stop(); return -1; } return 0; } /* AirTunes v2 streaming */ static struct raop_v2_packet * raop_v2_new_packet(void) { struct raop_v2_packet *pkt; if (pktbuf_size >= RETRANSMIT_BUFFER_SIZE) { pktbuf_size--; pkt = pktbuf_tail; pktbuf_tail = pktbuf_tail->prev; pktbuf_tail->next = NULL; } else { pkt = (struct raop_v2_packet *)malloc(sizeof(struct raop_v2_packet)); if (!pkt) { DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP packet\n"); return NULL; } } return pkt; } static struct raop_v2_packet * raop_v2_make_packet(uint8_t *rawbuf, uint64_t rtptime) { char ebuf[64]; struct raop_v2_packet *pkt; gpg_error_t gc_err; uint32_t rtptime32; uint16_t seq; pkt = raop_v2_new_packet(); if (!pkt) return NULL; memset(pkt, 0, sizeof(struct raop_v2_packet)); alac_encode(rawbuf, pkt->clear + AIRTUNES_V2_HDR_LEN, STOB(AIRTUNES_V2_PACKET_SAMPLES)); stream_seq++; pkt->seqnum = stream_seq; seq = htobe16(pkt->seqnum); rtptime32 = htobe32(RAOP_RTPTIME(rtptime)); pkt->clear[0] = 0x80; pkt->clear[1] = (sync_counter == 0) ? 0xe0 : 0x60; memcpy(pkt->clear + 2, &seq, 2); memcpy(pkt->clear + 4, &rtptime32, 4); /* RTP SSRC ID * Note: should htobe32() that value, but it's just a * random/unique ID so it's no big deal */ memcpy(pkt->clear + 8, &ssrc_id, 4); /* Copy AirTunes v2 header to encrypted packet */ memcpy(pkt->encrypted, pkt->clear, AIRTUNES_V2_HDR_LEN); /* Copy the tail of the audio packet that is left unencrypted */ memcpy(pkt->encrypted + AIRTUNES_V2_PKT_TAIL_OFF, pkt->clear + AIRTUNES_V2_PKT_TAIL_OFF, AIRTUNES_V2_PKT_TAIL_LEN); /* Reset cipher */ gc_err = gcry_cipher_reset(raop_aes_ctx); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not reset AES cipher: %s\n", ebuf); free(pkt); return NULL; } /* Set IV */ gc_err = gcry_cipher_setiv(raop_aes_ctx, raop_aes_iv, sizeof(raop_aes_iv)); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not set AES IV: %s\n", ebuf); free(pkt); return NULL; } /* Encrypt in blocks of 16 bytes */ gc_err = gcry_cipher_encrypt(raop_aes_ctx, pkt->encrypted + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16, pkt->clear + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not encrypt payload: %s\n", ebuf); free(pkt); return NULL; } pkt->prev = NULL; pkt->next = pktbuf_head; if (pktbuf_head) pktbuf_head->prev = pkt; if (!pktbuf_tail) pktbuf_tail = pkt; pktbuf_head = pkt; pktbuf_size++; return pkt; } static int raop_v2_send_packet(struct raop_session *rs, struct raop_v2_packet *pkt) { uint8_t *data; int ret; data = (rs->encrypt) ? pkt->encrypted : pkt->clear; ret = send(rs->server_fd, data, AIRTUNES_V2_PKT_LEN, 0); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Send error for %s: %s\n", rs->devname, strerror(errno)); raop_session_failure(rs); return -1; } else if (ret != AIRTUNES_V2_PKT_LEN) { DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for %s\n", ret, rs->devname); return -1; } return 0; } void raop_v2_write(uint8_t *buf, uint64_t rtptime) { struct raop_v2_packet *pkt; struct raop_session *rs; pkt = raop_v2_make_packet(buf, rtptime); if (!pkt) { raop_playback_stop(); return; } if (sync_counter == 126) { raop_v2_control_send_sync(rtptime, NULL); sync_counter = 1; } else sync_counter++; for (rs = sessions; rs; rs = rs->next) { if (rs->state != RAOP_STREAMING) continue; raop_v2_send_packet(rs, pkt); } return; } static void raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len) { struct raop_v2_packet *pktbuf; int ret; uint16_t distance; /* Check that seqnum is in the retransmit buffer */ if ((seqnum > pktbuf_head->seqnum) || (seqnum < pktbuf_tail->seqnum)) { DPRINTF(E_WARN, L_RAOP, "RAOP device %s asking for seqnum %u; not in buffer (h %u t %u)\n", rs->devname, seqnum, pktbuf_head->seqnum, pktbuf_tail->seqnum); return; } if (seqnum > pktbuf_head->seqnum) { distance = seqnum - pktbuf_tail->seqnum; if (distance > (RETRANSMIT_BUFFER_SIZE / 2)) pktbuf = pktbuf_head; else pktbuf = pktbuf_tail; } else { distance = pktbuf_head->seqnum - seqnum; if (distance > (RETRANSMIT_BUFFER_SIZE / 2)) pktbuf = pktbuf_tail; else pktbuf = pktbuf_head; } if (pktbuf == pktbuf_head) { while (pktbuf && seqnum != pktbuf->seqnum) pktbuf = pktbuf->next; } else { while (pktbuf && seqnum != pktbuf->seqnum) pktbuf = pktbuf->prev; } while (len && pktbuf) { ret = raop_v2_send_packet(rs, pktbuf); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Error retransmit packet, aborting retransmission\n"); return; } pktbuf = pktbuf->prev; len--; } if (len != 0) DPRINTF(E_LOG, L_RAOP, "WARNING: len non-zero at end of retransmission\n"); } static int raop_v2_stream_open(struct raop_session *rs) { int len; int ret; #ifdef __linux__ rs->server_fd = socket(rs->sa.ss.ss_family, SOCK_DGRAM | SOCK_CLOEXEC, 0); #else rs->server_fd = socket(rs->sa.ss.ss_family, SOCK_DGRAM, 0); #endif if (rs->server_fd < 0) { DPRINTF(E_LOG, L_RAOP, "Could not create socket for streaming: %s\n", strerror(errno)); return -1; } switch (rs->sa.ss.ss_family) { case AF_INET: rs->sa.sin.sin_port = htons(rs->server_port); len = sizeof(rs->sa.sin); break; case AF_INET6: rs->sa.sin6.sin6_port = htons(rs->server_port); len = sizeof(rs->sa.sin6); break; default: DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); goto out_fail; } ret = connect(rs->server_fd, &rs->sa.sa, len); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "connect() to [%s]:%u failed: %s\n", rs->address, rs->server_port, strerror(errno)); goto out_fail; } /* Include the device into the set of active devices if * playback is in progress. */ if (sync_counter != 0) rs->state = RAOP_STREAMING; else rs->state = RAOP_CONNECTED; return 0; out_fail: close(rs->server_fd); rs->server_fd = -1; return -1; } /* Session startup */ static void raop_startup_cancel(struct raop_session *rs) { /* Try being nice to our peer */ if (rs->session) raop_send_req_teardown(rs, raop_session_failure_cb); else raop_session_failure(rs); } static void raop_cb_startup_volume(struct evrtsp_request *req, void *arg) { raop_status_cb status_cb; struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SET_PARAMETER request failed for startup volume: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; raop_metadata_startup_send(rs); ret = raop_v2_stream_open(rs); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not open streaming socket\n"); goto cleanup; } /* Session startup and setup is done, tell our user */ status_cb = rs->status_cb; rs->status_cb = NULL; status_cb(rs->dev, rs, rs->state); if (!rs->reqs_in_flight) evrtsp_connection_set_closecb(rs->ctrl, raop_rtsp_close_cb, rs); return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_record(struct evrtsp_request *req, void *arg) { struct raop_session *rs; const char *param; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "RECORD request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; /* Audio latency */ param = evrtsp_find_header(req->input_headers, "Audio-Latency"); if (!param) DPRINTF(E_INFO, L_RAOP, "RECORD reply from %s did not have an Audio-Latency header\n", rs->devname); else DPRINTF(E_DBG, L_RAOP, "RAOP audio latency is %s\n", param); rs->state = RAOP_RECORD; /* Set initial volume */ raop_set_volume_internal(rs, rs->volume, raop_cb_startup_volume); return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_setup(struct evrtsp_request *req, void *arg) { struct raop_session *rs; const char *param; char *transport; char *token; char *ptr; int tmp; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "SETUP request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; /* Server-side session ID */ param = evrtsp_find_header(req->input_headers, "Session"); if (!param) { DPRINTF(E_LOG, L_RAOP, "Missing Session header in SETUP reply\n"); goto cleanup; } rs->session = strdup(param); /* Check transport and get remote streaming port */ param = evrtsp_find_header(req->input_headers, "Transport"); if (!param) { DPRINTF(E_LOG, L_RAOP, "Missing Transport header in SETUP reply\n"); goto cleanup; } /* Check transport is really UDP, AirTunes v2 streaming */ if (strncmp(param, "RTP/AVP/UDP;", strlen("RTP/AVP/UDP;")) != 0) { DPRINTF(E_LOG, L_RAOP, "ApEx replied with unsupported Transport: %s\n", param); goto cleanup; } transport = strdup(param); if (!transport) { DPRINTF(E_LOG, L_RAOP, "Out of memory for Transport header copy\n"); goto cleanup; } token = strchr(transport, ';'); token++; token = strtok_r(token, ";=", &ptr); while (token) { DPRINTF(E_DBG, L_RAOP, "token: %s\n", token); if (strcmp(token, "server_port") == 0) { token = strtok_r(NULL, ";=", &ptr); if (!token) break; ret = safe_atoi32(token, &tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not read server_port\n"); break; } rs->server_port = tmp; } else if (strcmp(token, "control_port") == 0) { token = strtok_r(NULL, ";=", &ptr); if (!token) break; ret = safe_atoi32(token, &tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not read control_port\n"); break; } rs->control_port = tmp; } else if (strcmp(token, "timing_port") == 0) { token = strtok_r(NULL, ";=", &ptr); if (!token) break; ret = safe_atoi32(token, &tmp); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not read timing_port\n"); break; } rs->timing_port = tmp; } token = strtok_r(NULL, ";=", &ptr); } free(transport); if ((rs->server_port == 0) || (rs->control_port == 0) || (rs->timing_port == 0)) { DPRINTF(E_LOG, L_RAOP, "Transport header lacked some port numbers in SETUP reply\n"); DPRINTF(E_LOG, L_RAOP, "Transport header was: %s\n", param); goto cleanup; } DPRINTF(E_DBG, L_RAOP, "Negotiated AirTunes v2 UDP streaming session %s; ports s=%u c=%u t=%u\n", rs->session, rs->server_port, rs->control_port, rs->timing_port); rs->state = RAOP_SETUP; /* Send RECORD */ ret = raop_send_req_record(rs, raop_cb_startup_record); if (ret < 0) goto cleanup; return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_announce(struct evrtsp_request *req, void *arg) { struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto cleanup; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "ANNOUNCE request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; rs->state = RAOP_ANNOUNCE; /* Send SETUP */ ret = raop_send_req_setup(rs, raop_cb_startup_setup); if (ret < 0) goto cleanup; return; cleanup: raop_startup_cancel(rs); } static void raop_cb_startup_options(struct evrtsp_request *req, void *arg) { struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto cleanup; if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED)) { DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed in session startup: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; if (req->response_code == RTSP_UNAUTHORIZED) { if (rs->req_has_auth) { DPRINTF(E_LOG, L_RAOP, "Bad password for device %s\n", rs->devname); rs->state = RAOP_PASSWORD; goto cleanup; } ret = raop_parse_auth(rs, req); if (ret < 0) goto cleanup; ret = raop_send_req_options(rs, raop_cb_startup_options); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not re-run OPTIONS request with authentication\n"); goto cleanup; } return; } rs->state = RAOP_OPTIONS; /* Send ANNOUNCE */ ret = raop_send_req_announce(rs, raop_cb_startup_announce); if (ret < 0) goto cleanup; return; cleanup: raop_startup_cancel(rs); } static void raop_cb_shutdown_teardown(struct evrtsp_request *req, void *arg) { raop_status_cb status_cb; struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto error; if (req->response_code != RTSP_OK) { DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown: %d %s\n", req->response_code, req->response_code_line); goto error; } ret = raop_check_cseq(rs, req); if (ret < 0) goto error; rs->state = RAOP_STOPPED; /* Session shut down, tell our user */ status_cb = rs->status_cb; rs->status_cb = NULL; status_cb(rs->dev, rs, rs->state); raop_session_cleanup(rs); return; error: raop_session_failure(rs); } static void raop_cb_probe_options(struct evrtsp_request *req, void *arg) { raop_status_cb status_cb; struct raop_session *rs; int ret; rs = (struct raop_session *)arg; rs->reqs_in_flight--; if (!req) goto cleanup; if ((req->response_code != RTSP_OK) && (req->response_code != RTSP_UNAUTHORIZED)) { DPRINTF(E_LOG, L_RAOP, "OPTIONS request failed in device probe: %d %s\n", req->response_code, req->response_code_line); goto cleanup; } ret = raop_check_cseq(rs, req); if (ret < 0) goto cleanup; if (req->response_code == RTSP_UNAUTHORIZED) { if (rs->req_has_auth) { DPRINTF(E_LOG, L_RAOP, "Bad password for device %s\n", rs->devname); rs->state = RAOP_PASSWORD; goto cleanup; } ret = raop_parse_auth(rs, req); if (ret < 0) goto cleanup; ret = raop_send_req_options(rs, raop_cb_probe_options); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not re-run OPTIONS request with authentication\n"); goto cleanup; } return; } rs->state = RAOP_OPTIONS; /* Device probed successfully, tell our user */ status_cb = rs->status_cb; rs->status_cb = NULL; status_cb(rs->dev, rs, rs->state); /* We're not going further with this session */ raop_session_cleanup(rs); return; cleanup: raop_session_failure(rs); } int raop_device_probe(struct raop_device *rd, raop_status_cb cb) { struct raop_session *rs; int ret; /* Send an OPTIONS request to test our ability to connect to the device, * including the need for and/or validity of the password */ rs = raop_session_make(rd, AF_INET6, cb); if (rs) { ret = raop_send_req_options(rs, raop_cb_probe_options); if (ret == 0) return 0; else { DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv6 (probe)\n"); raop_session_cleanup(rs); } } rs = raop_session_make(rd, AF_INET, cb); if (!rs) return -1; ret = raop_send_req_options(rs, raop_cb_probe_options); if (ret < 0) { DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv4 (probe)\n"); raop_session_cleanup(rs); return -1; } return 0; } int raop_device_start(struct raop_device *rd, raop_status_cb cb, uint64_t rtptime) { struct raop_session *rs; int ret; /* Send an OPTIONS request to establish the connection * After that, we can determine our local address and build our session URL * for all subsequent requests. */ rs = raop_session_make(rd, AF_INET6, cb); if (rs) { rs->start_rtptime = rtptime; ret = raop_send_req_options(rs, raop_cb_startup_options); if (ret == 0) return 0; else { DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv6 (start)\n"); raop_session_cleanup(rs); } } rs = raop_session_make(rd, AF_INET, cb); if (!rs) return -1; rs->start_rtptime = rtptime; ret = raop_send_req_options(rs, raop_cb_startup_options); if (ret < 0) { DPRINTF(E_WARN, L_RAOP, "Could not send OPTIONS request on IPv4 (start)\n"); raop_session_cleanup(rs); return -1; } return 0; } void raop_device_stop(struct raop_session *rs) { if (!(rs->state & RAOP_F_CONNECTED)) raop_session_cleanup(rs); else raop_send_req_teardown(rs, raop_cb_shutdown_teardown); } void raop_playback_start(uint64_t next_pkt, struct timespec *ts) { struct raop_session *rs; if (event_initialized(&flush_timer)) event_del(&flush_timer); sync_counter = 0; for (rs = sessions; rs; rs = rs->next) { if (rs->state == RAOP_CONNECTED) rs->state = RAOP_STREAMING; } /* Send initial playback sync */ raop_v2_control_send_sync(next_pkt, ts); } void raop_playback_stop(void) { struct raop_session *rs; int ret; for (rs = sessions; rs; rs = rs->next) { ret = raop_send_req_teardown(rs, raop_cb_shutdown_teardown); if (ret < 0) DPRINTF(E_LOG, L_RAOP, "shutdown: TEARDOWN request failed!\n"); } } void raop_set_status_cb(struct raop_session *rs, raop_status_cb cb) { rs->status_cb = cb; } int raop_init(int *v6enabled) { char ebuf[64]; char *ptr; char *libname; gpg_error_t gc_err; int ret; timing_4svc.fd = -1; timing_4svc.port = 0; timing_6svc.fd = -1; timing_6svc.port = 0; control_4svc.fd = -1; control_4svc.port = 0; control_6svc.fd = -1; control_6svc.port = 0; sessions = NULL; pktbuf_size = 0; pktbuf_head = NULL; pktbuf_tail = NULL; metadata_head = NULL; metadata_tail = NULL; /* Generate RTP SSRC ID from library name */ libname = cfg_getstr(cfg_getsec(cfg, "library"), "name"); ssrc_id = djb_hash(libname, strlen(libname)); /* Random RTP sequence start */ gcry_randomize(&stream_seq, sizeof(stream_seq), GCRY_STRONG_RANDOM); /* Generate AES key and IV */ gcry_randomize(raop_aes_key, sizeof(raop_aes_key), GCRY_STRONG_RANDOM); gcry_randomize(raop_aes_iv, sizeof(raop_aes_iv), GCRY_STRONG_RANDOM); /* Setup AES */ gc_err = gcry_cipher_open(&raop_aes_ctx, GCRY_CIPHER_AES, GCRY_CIPHER_MODE_CBC, 0); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not open AES cipher: %s\n", ebuf); return -1; } /* Set key */ gc_err = gcry_cipher_setkey(raop_aes_ctx, raop_aes_key, sizeof(raop_aes_key)); if (gc_err != GPG_ERR_NO_ERROR) { gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); DPRINTF(E_LOG, L_RAOP, "Could not set AES key: %s\n", ebuf); goto out_close_cipher; } /* Prepare Base64-encoded key & IV for SDP */ raop_aes_key_b64 = raop_crypt_encrypt_aes_key_base64(); if (!raop_aes_key_b64) { DPRINTF(E_LOG, L_RAOP, "Couldn't encrypt and encode AES session key\n"); goto out_close_cipher; } raop_aes_iv_b64 = b64_encode(raop_aes_iv, sizeof(raop_aes_iv)); if (!raop_aes_iv_b64) { DPRINTF(E_LOG, L_RAOP, "Couldn't encode AES IV\n"); goto out_free_b64_key; } /* Remove base64 padding */ ptr = strchr(raop_aes_key_b64, '='); if (ptr) *ptr = '\0'; ptr = strchr(raop_aes_iv_b64, '='); if (ptr) *ptr = '\0'; ret = raop_v2_timing_start(*v6enabled); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "AirTunes v2 time synchronization failed to start\n"); goto out_free_b64_iv; } ret = raop_v2_control_start(*v6enabled); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "AirTunes v2 playback synchronization failed to start\n"); goto out_stop_timing; } if (*v6enabled) *v6enabled = !((timing_6svc.fd < 0) || (control_6svc.fd < 0)); return 0; out_stop_timing: raop_v2_timing_stop(); out_free_b64_iv: free(raop_aes_iv_b64); out_free_b64_key: free(raop_aes_key_b64); out_close_cipher: gcry_cipher_close(raop_aes_ctx); return -1; } void raop_deinit(void) { struct raop_session *rs; for (rs = sessions; sessions; rs = sessions) { sessions = rs->next; raop_session_free(rs); } raop_v2_timing_stop(); raop_v2_control_stop(); gcry_cipher_close(raop_aes_ctx); free(raop_aes_key_b64); free(raop_aes_iv_b64); }