/** * Copyright (C) 2018 Juho Vähä-Herttua * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * *=================================================================== * modfied by fduncanh 2021-2023 */ /* This file should be only included from raop.c as it defines static handler * functions and depends on raop internals */ #include "dnssdint.h" #include "utils.h" #include #include #include #include #define AUDIO_SAMPLE_RATE 44100 /* all supported AirPlay audio format use this sample rate */ #define SECOND_IN_USECS 1000000 #define SECOND_IN_NSECS 1000000000 #define MAX_PW_ATTEMPTS 3 typedef void (*raop_handler_t)(raop_conn_t *, http_request_t *, http_response_t *, char **, int *); static void raop_handler_info(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; assert(raop->dnssd); /* There are three possible RTSP/1.0 GET/info requests (1) with a CSeq number and with a plist (2) with a CSeq number and without a plist (3) without a CSeq number or a Header (part of Bluetooth LE Service Discovery) */ const char* url = NULL; const char* content_type = NULL; const char* cseq = NULL; url = http_request_get_url(request); content_type = http_request_get_header(request, "Content-Type"); cseq = http_request_get_header(request, "CSeq"); int len = 0; bool add_txt_airplay = false; bool add_txt_raop = false; const char txtRAOP[] = "txtRAOP"; const char txtAirPlay[] = "txtAirPlay"; plist_t res_node = plist_new_dict(); /* initial GET/info request sends plist with string "txtAirPlay" */ if (content_type && strstr(content_type, "application/x-apple-binary-plist")) { char *qualifier_string = NULL; const char *data = NULL; int data_len = 0; data = http_request_get_data(request, &data_len); //parsing bplist plist_t req_root_node = NULL; plist_from_bin(data, data_len, &req_root_node); plist_t req_qualifier_node = plist_dict_get_item(req_root_node, "qualifier"); if (PLIST_IS_ARRAY(req_qualifier_node)) { plist_t req_string_node = plist_array_get_item(req_qualifier_node, 0); plist_get_string_val(req_string_node, &qualifier_string); } if (qualifier_string) { if (!strcmp(qualifier_string, txtAirPlay )) { add_txt_airplay = true; } else if (!strcmp(qualifier_string, txtRAOP)) { add_txt_raop = true; } plist_mem_free(qualifier_string); } plist_free(req_root_node); } /* Bluetoth LE discovery protocol request */ if (!cseq) { add_txt_airplay = (bool) strstr(url, txtAirPlay); add_txt_raop = (bool) strstr(url, txtRAOP); } if (add_txt_airplay) { const char *txt = dnssd_get_airplay_txt(raop->dnssd, &len); plist_t txt_airplay_node = plist_new_data(txt, len); plist_dict_set_item(res_node, txtAirPlay, txt_airplay_node); } if (add_txt_raop) { const char *txt = dnssd_get_raop_txt(raop->dnssd, &len); plist_t txt_raop_node = plist_new_data(txt, len); plist_dict_set_item(res_node, txtRAOP, txt_raop_node); } /* don't need anything below here in the response to initial "txtAirPlay" GET/info request */ if (content_type) { goto finished; } /* deviceID is the physical hardware address, and will not change */ int hw_addr_raw_len = 0; const char *hw_addr_raw = dnssd_get_hw_addr(raop->dnssd, &hw_addr_raw_len); char *hw_addr = calloc(1, 3 * hw_addr_raw_len); //int hw_addr_len = utils_hwaddr_airplay(hw_addr, 3 * hw_addr_raw_len, hw_addr_raw, hw_addr_raw_len); plist_t device_id_node = plist_new_string(hw_addr); plist_dict_set_item(res_node, "deviceID", device_id_node); plist_t mac_address_node = plist_new_string(hw_addr); plist_dict_set_item(res_node, "macAddress", mac_address_node); free(hw_addr); /* Persistent Public Key */ int pk_len = 0; char *pk = utils_parse_hex(raop->pk_str, strlen(raop->pk_str), &pk_len); plist_t pk_node = plist_new_data(pk, pk_len); plist_dict_set_item(res_node, "pk", pk_node); free(pk); uint64_t features = dnssd_get_airplay_features(raop->dnssd); plist_t features_node = plist_new_uint(features); plist_dict_set_item(res_node, "features", features_node); int name_len = 0; const char *name = dnssd_get_name(raop->dnssd, &name_len); plist_t name_node = plist_new_string(name); plist_dict_set_item(res_node, "name", name_node); plist_t pi_node = plist_new_string(AIRPLAY_PI); plist_dict_set_item(res_node, "pi", pi_node); plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10)); plist_dict_set_item(res_node, "vv", vv_node); plist_t status_flags_node = plist_new_uint(68); plist_dict_set_item(res_node, "statusFlags", status_flags_node); plist_t keep_alive_low_power_node = plist_new_uint(1); plist_dict_set_item(res_node, "keepAliveLowPower", keep_alive_low_power_node); plist_t source_version_node = plist_new_string(GLOBAL_VERSION); plist_dict_set_item(res_node, "sourceVersion", source_version_node); plist_t keep_alive_send_stats_as_body_node = plist_new_bool(1); plist_dict_set_item(res_node, "keepAliveSendStatsAsBody", keep_alive_send_stats_as_body_node); plist_t model_node = plist_new_string(GLOBAL_MODEL); plist_dict_set_item(res_node, "model", model_node); /* dont need anything below here in the Bluetooth LE response */ if (cseq == NULL) { goto finished; } plist_t initial_volume_node = plist_new_real(raop->callbacks.audio_set_client_volume(raop->callbacks.cls)); plist_dict_set_item(res_node, "initialVolume", initial_volume_node); plist_t audio_latencies_node = plist_new_array(); plist_t audio_latencies_0_node = plist_new_dict(); plist_t audio_latencies_0_output_latency_micros_node = plist_new_bool(0); plist_t audio_latencies_0_type_node = plist_new_uint(100); plist_t audio_latencies_0_audio_type_node = plist_new_string("default"); plist_t audio_latencies_0_input_latency_micros_node = plist_new_uint(0); plist_dict_set_item(audio_latencies_0_node, "type", audio_latencies_0_type_node); plist_dict_set_item(audio_latencies_0_node, "inputLatencyMicros", audio_latencies_0_input_latency_micros_node); plist_dict_set_item(audio_latencies_0_node, "audioType", audio_latencies_0_audio_type_node); plist_dict_set_item(audio_latencies_0_node, "outputLatencyMicros", audio_latencies_0_output_latency_micros_node); plist_array_append_item(audio_latencies_node, audio_latencies_0_node); plist_t audio_latencies_1_node = plist_new_dict(); plist_t audio_latencies_1_output_latency_micros_node = plist_new_bool(0); plist_t audio_latencies_1_type_node = plist_new_uint(101); plist_t audio_latencies_1_audio_type_node = plist_new_string("default"); plist_t audio_latencies_1_input_latency_micros_node = plist_new_uint(0); plist_dict_set_item(audio_latencies_1_node, "type", audio_latencies_1_type_node); plist_dict_set_item(audio_latencies_1_node, "audioType", audio_latencies_1_audio_type_node); plist_dict_set_item(audio_latencies_1_node, "inputLatencyMicros", audio_latencies_1_input_latency_micros_node); plist_dict_set_item(audio_latencies_1_node, "outputLatencyMicros", audio_latencies_1_output_latency_micros_node); plist_array_append_item(audio_latencies_node, audio_latencies_1_node); plist_dict_set_item(res_node, "audioLatencies", audio_latencies_node); plist_t audio_formats_node = plist_new_array(); plist_t audio_format_0_node = plist_new_dict(); plist_t audio_format_0_type_node = plist_new_uint(100); plist_t audio_format_0_audio_input_formats_node = plist_new_uint(0x3fffffc); plist_t audio_format_0_audio_output_formats_node = plist_new_uint(0x3fffffc); plist_dict_set_item(audio_format_0_node, "audioOutputFormats", audio_format_0_audio_output_formats_node); plist_dict_set_item(audio_format_0_node, "type", audio_format_0_type_node); plist_dict_set_item(audio_format_0_node, "audioInputFormats", audio_format_0_audio_input_formats_node); plist_array_append_item(audio_formats_node, audio_format_0_node); plist_t audio_format_1_node = plist_new_dict(); plist_t audio_format_1_type_node = plist_new_uint(101); plist_t audio_format_1_audio_input_formats_node = plist_new_uint(0x3fffffc); plist_t audio_format_1_audio_output_formats_node = plist_new_uint(0x3fffffc); plist_dict_set_item(audio_format_1_node, "audioOutputFormats", audio_format_1_audio_output_formats_node); plist_dict_set_item(audio_format_1_node, "type", audio_format_1_type_node); plist_dict_set_item(audio_format_1_node, "audioInputFormats", audio_format_1_audio_input_formats_node); plist_array_append_item(audio_formats_node, audio_format_1_node); plist_dict_set_item(res_node, "audioFormats", audio_formats_node); plist_t displays_node = plist_new_array(); plist_t displays_0_node = plist_new_dict(); plist_t displays_0_width_physical_node = plist_new_uint(0); plist_t displays_0_height_physical_node = plist_new_uint(0); plist_t displays_0_uuid_node = plist_new_string("e0ff8a27-6738-3d56-8a16-cc53aacee925"); plist_t displays_0_width_node = plist_new_uint(raop->width); plist_t displays_0_height_node = plist_new_uint(raop->height); plist_t displays_0_width_pixels_node = plist_new_uint(raop->width); plist_t displays_0_height_pixels_node = plist_new_uint(raop->height); plist_t displays_0_rotation_node = plist_new_bool(0); /* set to true in AppleTV gen 3 (which has features bit 8 set */ plist_t displays_0_refresh_rate_node = plist_new_real((double) 1.0 / raop->refreshRate); /* set as real 0.166666 = 60hz in AppleTV gen 3 */ plist_t displays_0_max_fps_node = plist_new_uint(raop->maxFPS); plist_t displays_0_overscanned_node = plist_new_bool(raop->overscanned); plist_t displays_0_features = plist_new_uint(14); plist_dict_set_item(displays_0_node, "uuid", displays_0_uuid_node); plist_dict_set_item(displays_0_node, "widthPhysical", displays_0_width_physical_node); plist_dict_set_item(displays_0_node, "heightPhysical", displays_0_height_physical_node); plist_dict_set_item(displays_0_node, "width", displays_0_width_node); plist_dict_set_item(displays_0_node, "height", displays_0_height_node); plist_dict_set_item(displays_0_node, "widthPixels", displays_0_width_pixels_node); plist_dict_set_item(displays_0_node, "heightPixels", displays_0_height_pixels_node); plist_dict_set_item(displays_0_node, "rotation", displays_0_rotation_node); plist_dict_set_item(displays_0_node, "refreshRate", displays_0_refresh_rate_node); plist_dict_set_item(displays_0_node, "maxFPS", displays_0_max_fps_node); plist_dict_set_item(displays_0_node, "overscanned", displays_0_overscanned_node); plist_dict_set_item(displays_0_node, "features", displays_0_features); plist_array_append_item(displays_node, displays_0_node); plist_dict_set_item(res_node, "displays", displays_node); finished: plist_to_bin(res_node, response_data, (uint32_t *) response_datalen); plist_free(res_node); http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist"); } static void raop_handler_pairpinstart(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; logger_log(raop->logger, LOGGER_INFO, "client sent PAIR-PIN-START request"); int pin_4 = 0; if (raop->pin > 9999) { pin_4 = raop->pin % 10000; } else { pin_4 = random_pin(); if (pin_4 < 0) { logger_log(raop->logger, LOGGER_ERR, "Failed to generate random pin"); } else { raop->pin = (unsigned short) pin_4 % 10000; } } char pin[6] = { '\0' }; snprintf(pin, 5, "%04u", pin_4); if (raop->callbacks.display_pin) { raop->callbacks.display_pin(raop->callbacks.cls, pin); } logger_log(raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", pin); } static void raop_handler_pairsetup_pin(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const char *request_data = NULL;; int request_datalen = 0; bool data_is_plist = false; bool logger_debug = (logger_get_level(raop->logger) >= LOGGER_DEBUG); request_data = http_request_get_data(request, &request_datalen); logger_log(raop->logger, LOGGER_INFO, "client requested pair-setup-pin, datalen = %d", request_datalen); if (request_datalen > 0) { char *header_str= NULL; http_request_get_header_string(request, &header_str); logger_log(raop->logger, LOGGER_INFO, "request header: %s", header_str); data_is_plist = (strstr(header_str,"apple-binary-plist") != NULL); free(header_str); } if (!data_is_plist) { logger_log(raop->logger, LOGGER_INFO, "did not receive expected plist from client, request_datalen = %d", request_datalen); goto authentication_failed; } /* process the pair-setup-pin request */ plist_t req_root_node = NULL; plist_from_bin(request_data, request_datalen, &req_root_node); plist_t req_method_node = plist_dict_get_item(req_root_node, "method"); plist_t req_user_node = plist_dict_get_item(req_root_node, "user"); plist_t req_pk_node = plist_dict_get_item(req_root_node, "pk"); plist_t req_proof_node = plist_dict_get_item(req_root_node, "proof"); plist_t req_epk_node = plist_dict_get_item(req_root_node, "epk"); plist_t req_authtag_node = plist_dict_get_item(req_root_node, "authTag"); if (PLIST_IS_STRING(req_method_node) && PLIST_IS_STRING(req_user_node)) { /* this is the initial pair-setup-pin request */ const char *salt = NULL; char pin[6] = { '\0' }; const char *pk = NULL; int len_pk = 0, len_salt = 0; char *method = NULL; char *user = NULL; plist_get_string_val(req_method_node, &method); if (strncmp(method, "pin", strlen (method))) { logger_log(raop->logger, LOGGER_ERR, "error, required method is \"pin\", client requested \"%s\"", method); free (method); plist_free (req_root_node); return; } plist_mem_free(method); method = NULL; plist_get_string_val(req_user_node, &user); logger_log(raop->logger, LOGGER_INFO, "pair-setup-pin: device_id = %s", user); snprintf(pin, 6, "%04u", raop->pin % 10000); if (raop->pin < 10000) { raop->pin = 0; } int ret = srp_new_user(conn->session, raop->pairing, (const char *) user, (const char *) pin, &salt, &len_salt, &pk, &len_pk); plist_mem_free(user); user = NULL; plist_free(req_root_node); if (ret < 0) { logger_log(raop->logger, LOGGER_ERR, "failed to create user, err = %d", ret); goto authentication_failed; } plist_t res_root_node = plist_new_dict(); plist_t res_salt_node = plist_new_data(salt, len_salt); plist_t res_pk_node = plist_new_data(pk, len_pk); plist_dict_set_item(res_root_node, "pk", res_pk_node); plist_dict_set_item(res_root_node, "salt", res_salt_node); plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen); plist_free(res_root_node); http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist"); return; } else if (PLIST_IS_DATA(req_pk_node) && PLIST_IS_DATA(req_proof_node)) { /* this is the second part of pair-setup-pin request */ char *client_pk = NULL; char *client_proof = NULL; unsigned char proof[64] = { '\0' }; memset(proof, 0, sizeof(proof)); uint64_t client_pk_len = 0; uint64_t client_proof_len = 0; plist_get_data_val(req_pk_node, &client_pk, &client_pk_len); plist_get_data_val(req_proof_node, &client_proof, &client_proof_len); if (logger_debug) { char *str = utils_data_to_string((const unsigned char *) client_proof, client_proof_len, 20); logger_log(raop->logger, LOGGER_DEBUG, "client SRP6a proof :\n%s", str); free (str); } memcpy(proof, client_proof, (int) client_proof_len); free (client_proof); int ret = srp_validate_proof(conn->session, raop->pairing, (const unsigned char *) client_pk, (int) client_pk_len, proof, (int) client_proof_len, (int) sizeof(proof)); free (client_pk); plist_free(req_root_node); if (ret < 0) { logger_log(raop->logger, LOGGER_ERR, "Client Authentication Failure (client proof not validated)"); goto authentication_failed; } if (logger_debug) { char *str = utils_data_to_string((const unsigned char *) proof, sizeof(proof), 20); logger_log(raop->logger, LOGGER_DEBUG, "server SRP6a proof :\n%s", str); free (str); } plist_t res_root_node = plist_new_dict(); plist_t res_proof_node = plist_new_data((const char *) proof, 20); plist_dict_set_item(res_root_node, "proof", res_proof_node); plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen); plist_free(res_root_node); http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist"); return; } else if (PLIST_IS_DATA(req_epk_node) && PLIST_IS_DATA(req_authtag_node)) { /* this is the third part of pair-setup-pin request */ char *client_epk = NULL; char *client_authtag = NULL; uint64_t client_epk_len = 0; uint64_t client_authtag_len = 0; unsigned char epk[ED25519_KEY_SIZE]; unsigned char authtag[GCM_AUTHTAG_SIZE]; int ret = 0; plist_get_data_val(req_epk_node, &client_epk, &client_epk_len); plist_get_data_val(req_authtag_node, &client_authtag, &client_authtag_len); if (logger_debug) { char *str = utils_data_to_string((const unsigned char *) client_epk, client_epk_len, 16); logger_log(raop->logger, LOGGER_DEBUG, "client_epk %d:\n%s\n", (int) client_epk_len, str); str = utils_data_to_string((const unsigned char *) client_authtag, client_authtag_len, 16); logger_log(raop->logger, LOGGER_DEBUG, "client_authtag %d:\n%s\n", (int) client_authtag_len, str); free (str); } memcpy(epk, client_epk, ED25519_KEY_SIZE); memcpy(authtag, client_authtag, GCM_AUTHTAG_SIZE); free (client_authtag); free (client_epk); plist_free(req_root_node); ret = srp_confirm_pair_setup(conn->session, raop->pairing, epk, authtag); if (ret < 0) { logger_log(raop->logger, LOGGER_ERR, "pair-pin-setup (step 3): client authentication failed\n"); goto authentication_failed; } else { logger_log(raop->logger, LOGGER_DEBUG, "pair-pin-setup success\n"); } pairing_session_set_setup_status(conn->session); plist_t res_root_node = plist_new_dict(); plist_t res_epk_node = plist_new_data((const char *) epk, 32); plist_t res_authtag_node = plist_new_data((const char *) authtag, 16); plist_dict_set_item(res_root_node, "epk", res_epk_node); plist_dict_set_item(res_root_node, "authTag", res_authtag_node); plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen); plist_free(res_root_node); http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist"); return; } authentication_failed:; http_response_init(response, "RTSP/1.0", 470, "Client Authentication Failure"); } static void raop_handler_pairsetup(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; unsigned char public_key[ED25519_KEY_SIZE]; //const char *data; int datalen = 0; //data = http_request_get_data(request, &datalen); if (datalen != 32) { logger_log(raop->logger, LOGGER_ERR, "Invalid pair-setup data"); return; } pairing_get_public_key(raop->pairing, public_key); pairing_session_set_setup_status(conn->session); *response_data = calloc(1, sizeof(public_key)); if (*response_data) { http_response_add_header(response, "Content-Type", "application/octet-stream"); memcpy(*response_data, public_key, sizeof(public_key)); *response_datalen = sizeof(public_key); } } static void raop_handler_pairverify(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; bool register_check = false; if (pairing_session_check_handshake_status(conn->session)) { if (raop->use_pin) { pairing_session_set_setup_status(conn->session); register_check = true; } else { return; } } unsigned char public_key[X25519_KEY_SIZE]; unsigned char signature[PAIRING_SIG_SIZE]; const unsigned char *data = NULL; int datalen = 0; data = (unsigned char *) http_request_get_data(request, &datalen); if (datalen < 4) { logger_log(raop->logger, LOGGER_ERR, "Invalid pair-verify data"); return; } switch (data[0]) { case 1: if (datalen != 4 + X25519_KEY_SIZE + ED25519_KEY_SIZE) { logger_log(raop->logger, LOGGER_ERR, "Invalid pair-verify data"); return; } /* We can fall through these errors, the result will just be garbage... */ if (pairing_session_handshake(conn->session, data + 4, data + 4 + X25519_KEY_SIZE)) { logger_log(raop->logger, LOGGER_ERR, "Error initializing pair-verify handshake"); } if (pairing_session_get_public_key(conn->session, public_key)) { logger_log(raop->logger, LOGGER_ERR, "Error getting ECDH public key"); } if (pairing_session_get_signature(conn->session, signature)) { logger_log(raop->logger, LOGGER_ERR, "Error getting ED25519 signature"); } if (register_check) { bool registered_client = true; if (raop->callbacks.check_register) { const unsigned char *pk = data + 4 + X25519_KEY_SIZE; char *pk64 = NULL; ed25519_pk_to_base64(pk, &pk64); registered_client = raop->callbacks.check_register(raop->callbacks.cls, pk64); free (pk64); } if (!registered_client) { return; } } *response_data = calloc(1, sizeof(public_key) + sizeof(signature)); if (*response_data) { http_response_add_header(response, "Content-Type", "application/octet-stream"); memcpy(*response_data, public_key, sizeof(public_key)); memcpy(*response_data + sizeof(public_key), signature, sizeof(signature)); *response_datalen = sizeof(public_key) + sizeof(signature); } break; case 0: logger_log(raop->logger, LOGGER_DEBUG, "2nd pair-verify step: checking signature"); if (datalen != 4 + PAIRING_SIG_SIZE) { logger_log(raop->logger, LOGGER_ERR, "Invalid pair-verify data"); return; } if (pairing_session_finish(conn->session, data + 4)) { logger_log(raop->logger, LOGGER_ERR, "Incorrect pair-verify signature"); http_response_set_disconnect(response, 1); return; } logger_log(raop->logger, LOGGER_DEBUG, "pair-verify: signature is verified"); http_response_add_header(response, "Content-Type", "application/octet-stream"); break; } } static void raop_handler_fpsetup(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const unsigned char *data = NULL; int datalen = 0; data = (unsigned char *) http_request_get_data(request, &datalen); if (datalen == 16) { *response_data = calloc(142, sizeof(char)); if (*response_data) { http_response_add_header(response, "Content-Type", "application/octet-stream"); if (!fairplay_setup(conn->fairplay, data, (unsigned char *) *response_data)) { *response_datalen = 142; } else { // Handle error? free(*response_data); *response_data = NULL; } } } else if (datalen == 164) { *response_data = calloc(32, sizeof(char)); if (*response_data) { http_response_add_header(response, "Content-Type", "application/octet-stream"); if (!fairplay_handshake(conn->fairplay, data, (unsigned char *) *response_data)) { *response_datalen = 32; } else { // Handle error? free(*response_data); *response_data = NULL; } } } else { logger_log(raop->logger, LOGGER_ERR, "Invalid fp-setup data length"); return; } } static void raop_handler_options(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { http_response_add_header(response, "Public", "SETUP, RECORD, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER"); } static void raop_handler_setup(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const char *dacp_id = NULL; const char *active_remote_header = NULL; bool logger_debug = (logger_get_level(raop->logger) >= LOGGER_DEBUG); const char *data = NULL; int data_len = 0; data = http_request_get_data(request, &data_len); dacp_id = http_request_get_header(request, "DACP-ID"); active_remote_header = http_request_get_header(request, "Active-Remote"); if (dacp_id && active_remote_header) { logger_log(raop->logger, LOGGER_DEBUG, "DACP-ID: %s", dacp_id); logger_log(raop->logger, LOGGER_DEBUG, "Active-Remote: %s", active_remote_header); if (conn->raop_rtp) { raop_rtp_remote_control_id(conn->raop_rtp, dacp_id, active_remote_header); } } // Parsing bplist plist_t req_root_node = NULL; plist_from_bin(data, data_len, &req_root_node); plist_t req_ekey_node = plist_dict_get_item(req_root_node, "ekey"); plist_t req_eiv_node = plist_dict_get_item(req_root_node, "eiv"); // For the response plist_t res_root_node = plist_new_dict(); if (PLIST_IS_DATA(req_eiv_node) && PLIST_IS_DATA(req_ekey_node)) { // The first SETUP call that initializes keys and timing unsigned char aesiv[16] = { 0 }; unsigned char aeskey[16] = { 0 }; unsigned char eaeskey[72] = { 0 }; logger_log(raop->logger, LOGGER_DEBUG, "SETUP 1"); // First setup char *deviceID = NULL; plist_t req_deviceid_node = plist_dict_get_item(req_root_node, "deviceID"); plist_get_string_val(req_deviceid_node, &deviceID); /* RFC2617 Digest authentication (md5 hash) of uxplay client-access password, if set */ if (!conn->authenticated && raop->callbacks.passwd) { size_t pin_len = 4; const char *authorization = NULL; authorization = http_request_get_header(request, "Authorization"); if (!authorization) { // if random_pw is set, but client has changed, unset it if (raop->random_pw && strncmp(raop->random_pw + pin_len + 1, deviceID, 17)) { free(raop->random_pw); raop->random_pw = NULL; } } int len = 0; const char *password = raop->callbacks.passwd(raop->callbacks.cls, &len); // len = -1 means use a random password for this connection; len = 0 means no password if (len == -1 && raop->random_pw && raop->auth_fail_count >= MAX_PW_ATTEMPTS) { // change random_pw after MAX_PW_ATTEMPTS failed authentication attempts logger_log(raop->logger, LOGGER_INFO, "Too many authentication failures: generate new random password"); free(raop->random_pw); raop->random_pw = NULL; } if (len == -1 && !raop->random_pw) { // get and store 4 random digits int pin_4 = random_pin(); if (pin_4 < 0) { logger_log(raop->logger, LOGGER_ERR, "Failed to generate random pin"); pin_4 = 1234; } if ((raop->random_pw = (char *) calloc(pin_len + 1 + 18, sizeof(char)))) { char *pin = raop->random_pw; snprintf(pin, pin_len + 1, "%04u", pin_4 % 10000); pin[pin_len] = '\0'; snprintf(pin + pin_len + 1, 18, "%s", deviceID); } else { logger_log(raop->logger, LOGGER_ERR, "Failed to allocate raop->random_pw"); } raop->auth_fail_count = 0; } if (len == -1 && !authorization && raop->random_pw) { if (raop->callbacks.display_pin) { raop->callbacks.display_pin(raop->callbacks.cls, raop->random_pw); } logger_log(raop->logger, LOGGER_INFO, "*** CLIENT MUST NOW ENTER PIN = \"%s\" AS AIRPLAY PASSWORD", raop->random_pw); raop->auth_fail_count++; } if (len && !conn->authenticated) { if (len == -1) { password = (const char *) raop->random_pw; } char nonce_string[33] = { '\0' }; //bool stale = false; //not implemented if (len && authorization) { const char *ptr = strstr(authorization, "nonce=\"") + strlen("nonce=\""); strncpy(nonce_string, ptr, 32); const char *method = http_request_get_method(request); conn->authenticated = pairing_digest_verify(method, authorization, password); if (!conn->authenticated) { // if random_pw is used, the auth_fail_count will be the number of times it is displayed after creation if (len != -1) { raop->auth_fail_count++; } logger_log(raop->logger, LOGGER_INFO, "*** authentication failure: count = %u", raop->auth_fail_count); } if (conn->authenticated) { //printf("initial authenticatication OK\n"); conn->authenticated = conn->authenticated && !strcmp(nonce_string, raop->nonce); if (!conn->authenticated) { logger_log(raop->logger, LOGGER_INFO, "authentication rejected (nonce mismatch) %s %s", nonce_string, raop->nonce); } } if (conn->authenticated && raop->random_pw) { free (raop->random_pw); raop->random_pw = NULL; } if (raop->nonce) { free(raop->nonce); raop->nonce = NULL; } logger_log(raop->logger, LOGGER_INFO, "Client authentication %s", (conn->authenticated ? "success" : "failure")); } if (!conn->authenticated) { /* create a nonce */ const char *url = http_request_get_url(request); unsigned char nonce[16] = { '\0' }; int len = 16; uint64_t now = raop_ntp_get_local_time(); assert (!pairing_session_make_nonce(conn->session, &now, url, nonce, len)); if (raop->nonce) { free(raop->nonce); } raop->nonce = utils_hex_to_string(nonce, len); char response_text[80] = "Digest realm=\"raop\", nonce=\""; strncat(response_text, raop->nonce, 80 - strlen(response_text) - 1); strncat(response_text, "\"", 80 - strlen(response_text) - 1); http_response_init(response, "RTSP/1.0", 401, "Unauthorized"); http_response_add_header(response, "WWW-Authenticate", response_text); return; } } } char* eiv = NULL; uint64_t eiv_len = 0; char *model = NULL; char *name = NULL; bool admit_client = true; plist_t req_model_node = plist_dict_get_item(req_root_node, "model"); plist_get_string_val(req_model_node, &model); plist_t req_name_node = plist_dict_get_item(req_root_node, "name"); plist_get_string_val(req_name_node, &name); if (raop->callbacks.report_client_request) { raop->callbacks.report_client_request(raop->callbacks.cls, deviceID, model, name, &admit_client); } if (admit_client && deviceID && name && raop->callbacks.register_client) { char *client_device_id = NULL; char *client_pk = NULL; /* encoded as null-terminated base64 string, must be freed*/ get_pairing_session_client_data(conn->session, &client_device_id, &client_pk); if (client_pk && !strcmp(deviceID, client_device_id)) { raop->callbacks.register_client(raop->callbacks.cls, client_device_id, client_pk, name); free (client_pk); } } plist_mem_free(deviceID); deviceID = NULL; plist_mem_free(model); model = NULL; plist_mem_free(name); name = NULL; if (admit_client == false) { /* client is not authorized to connect */ plist_free(res_root_node); plist_free(req_root_node); return; } plist_get_data_val(req_eiv_node, &eiv, &eiv_len); memcpy(aesiv, eiv, 16); free(eiv); logger_log(raop->logger, LOGGER_DEBUG, "eiv_len = %llu", eiv_len); if (logger_debug) { char* str = utils_data_to_string(aesiv, 16, 16); logger_log(raop->logger, LOGGER_DEBUG, "16 byte aesiv (needed for AES-CBC audio decryption iv):\n%s", str); free(str); } char* ekey = NULL; uint64_t ekey_len = 0; plist_get_data_val(req_ekey_node, &ekey, &ekey_len); memcpy(eaeskey,ekey,72); free(ekey); logger_log(raop->logger, LOGGER_DEBUG, "ekey_len = %llu", ekey_len); // eaeskey is 72 bytes, aeskey is 16 bytes if (logger_debug) { char *str = utils_data_to_string((unsigned char *) eaeskey, ekey_len, 16); logger_log(raop->logger, LOGGER_DEBUG, "ekey:\n%s", str); free (str); } int ret = fairplay_decrypt(conn->fairplay, (unsigned char*) eaeskey, aeskey); logger_log(raop->logger, LOGGER_DEBUG, "fairplay_decrypt ret = %d", ret); if (logger_debug) { char *str = utils_data_to_string(aeskey, 16, 16); logger_log(raop->logger, LOGGER_DEBUG, "16 byte aeskey (fairplay-decrypted from ekey):\n%s", str); free(str); } const char *user_agent = http_request_get_header(request, "User-Agent"); logger_log(raop->logger, LOGGER_INFO, "Client identified as User-Agent: %s", user_agent); bool old_protocol = false; #ifdef OLD_PROTOCOL_CLIENT_USER_AGENT_LIST /* set in global.h */ if (strstr(OLD_PROTOCOL_CLIENT_USER_AGENT_LIST, user_agent)) old_protocol = true; if (strstr(user_agent, "AirMyPC")) old_protocol = true; //AirMyPC/7200 still uses old protocol: unlikely to change (?) #endif if (old_protocol) { /* some windows AirPlay-client emulators use old AirPlay 1 protocol with unhashed AES key */ logger_log(raop->logger, LOGGER_INFO, "Client identifed as using old protocol (unhashed) AES audio key)"); } else { unsigned char ecdh_secret[X25519_KEY_SIZE]; if (pairing_get_ecdh_secret_key(conn->session, ecdh_secret)) { /* In this case (legacy) pairing with client was successfully set up and created the shared ecdh_secret: * aeskey must now be hashed with it * * If byte 27 of features ("supports legacy pairing") is turned off, the client does not request pairsetup * and does NOT set up pairing (this eliminates a 5 second delay in connecting with no apparent bad effects). * In this case, ecdh_secret does not exist, so aeskey should NOT be hashed with it. * UxPlay may be able to function with byte 27 turned off because it currently does not support connections * with more than one client at a time. AppleTV supports up to 12 clients, uses pairing to give each a distinct * SessionID and ecdh_secret. * The "old protocol" Windows AirPlay client AirMyPC seems not to respect the byte 27 setting, and always sets * up the ecdh_secret, but decryption fails if aeskey is hashed.*/ if (logger_debug) { char *str = utils_data_to_string(ecdh_secret, X25519_KEY_SIZE, 16); logger_log(raop->logger, LOGGER_DEBUG, "32 byte shared ecdh_secret:\n%s", str); free(str); } memcpy(eaeskey, aeskey, 16); sha_ctx_t *ctx = sha_init(); sha_update(ctx, eaeskey, 16); sha_update(ctx, ecdh_secret, 32); sha_final(ctx, eaeskey, NULL); sha_destroy(ctx); memcpy(aeskey, eaeskey, 16); if (logger_debug) { char *str = utils_data_to_string(aeskey, 16, 16); logger_log(raop->logger, LOGGER_DEBUG, "16 byte aeskey after sha-256 hash with ecdh_secret:\n%s", str); free(str); } } } // Time port plist_t req_is_remote_control_only_node = plist_dict_get_item(req_root_node, "isRemoteControlOnly"); if (req_is_remote_control_only_node) { uint8_t bool_val = 0; plist_get_bool_val(req_is_remote_control_only_node, &bool_val); if (bool_val) { logger_log(raop->logger, LOGGER_ERR, "Client specified AirPlay2 \"Remote Control\" protocol\n" " Only AirPlay v1 protocol (using NTP and timing port) is supported"); } } char *timing_protocol = NULL; timing_protocol_t time_protocol = TP_NONE; plist_t req_timing_protocol_node = plist_dict_get_item(req_root_node, "timingProtocol"); plist_get_string_val(req_timing_protocol_node, &timing_protocol); if (timing_protocol) { int string_len = strlen(timing_protocol); if (strncmp(timing_protocol, "NTP", string_len) == 0) { time_protocol = NTP; } else if (strncmp(timing_protocol, "None", string_len) == 0) { time_protocol = TP_NONE; } else { time_protocol = TP_OTHER; } if (time_protocol != NTP) { logger_log(raop->logger, LOGGER_ERR, "Client specified timingProtocol=%s," " but timingProtocol= NTP is required here", timing_protocol); } plist_mem_free (timing_protocol); timing_protocol = NULL; } else { logger_log(raop->logger, LOGGER_DEBUG, "Client did not specify timingProtocol," " old protocol without offset will be used"); time_protocol = TP_UNSPECIFIED; } uint64_t timing_rport = 0; plist_t req_timing_port_node = plist_dict_get_item(req_root_node, "timingPort"); if (req_timing_port_node) { plist_get_uint_val(req_timing_port_node, &timing_rport); } if (timing_rport) { logger_log(raop->logger, LOGGER_DEBUG, "timing_rport = %llu", timing_rport); } else { logger_log(raop->logger, LOGGER_ERR, "Client did not supply timing_rport," " may be using unsupported AirPlay2 \"Remote Control\" protocol"); } unsigned short timing_lport = raop->timing_lport; conn->raop_ntp = NULL; conn->raop_rtp = NULL; conn->raop_rtp_mirror = NULL; char remote[40] = { 0 }; int len = utils_ipaddress_to_string(conn->remotelen, conn->remote, conn->zone_id, remote, (int) sizeof(remote)); if (!len || len > (int) sizeof(remote)) { char *str = utils_data_to_string(conn->remote, conn->remotelen, 16); logger_log(raop->logger, LOGGER_ERR, "failed to extract valid client ip address:\n" "*** UxPlay will be unable to send communications to client.\n" "*** address length %d, zone_id %u address data:\n%sparser returned \"%s\"\n", conn->remotelen, conn->zone_id, str, remote); free(str); } conn->raop_ntp = raop_ntp_init(raop->logger, &raop->callbacks, remote, conn->remotelen, (unsigned short) timing_rport, &time_protocol); raop_ntp_start(conn->raop_ntp, &timing_lport); conn->raop_rtp = raop_rtp_init(raop->logger, &raop->callbacks, conn->raop_ntp, remote, conn->remotelen, aeskey, aesiv); conn->raop_rtp_mirror = raop_rtp_mirror_init(raop->logger, &raop->callbacks, conn->raop_ntp, remote, conn->remotelen, aeskey); /* the event port is not used in mirror mode or audio mode */ unsigned short event_port = 0; plist_t res_event_port_node = plist_new_uint(event_port); plist_t res_timing_port_node = plist_new_uint(timing_lport); plist_dict_set_item(res_root_node, "timingPort", res_timing_port_node); plist_dict_set_item(res_root_node, "eventPort", res_event_port_node); logger_log(raop->logger, LOGGER_DEBUG, "eport = %d, tport = %d", event_port, timing_lport); } // Process stream setup requests plist_t req_streams_node = plist_dict_get_item(req_root_node, "streams"); if (PLIST_IS_ARRAY(req_streams_node)) { plist_t res_streams_node = plist_new_array(); int count = plist_array_get_size(req_streams_node); for (int i = 0; i < count; i++) { plist_t req_stream_node = plist_array_get_item(req_streams_node, i); plist_t req_stream_type_node = plist_dict_get_item(req_stream_node, "type"); uint64_t type; plist_get_uint_val(req_stream_type_node, &type); logger_log(raop->logger, LOGGER_DEBUG, "type = %llu", type); switch (type) { case 110: { // Mirroring raop_destroy_airplay_video(raop, -1); //cleanup any hls data still present when mirror video starts unsigned short dport = raop->mirror_data_lport; plist_t stream_id_node = plist_dict_get_item(req_stream_node, "streamConnectionID"); uint64_t stream_connection_id = 0; plist_get_uint_val(stream_id_node, &stream_connection_id); logger_log(raop->logger, LOGGER_DEBUG, "streamConnectionID (needed for AES-CTR video decryption" " key and iv): %llu", stream_connection_id); if (conn->raop_rtp_mirror) { raop_rtp_mirror_init_aes(conn->raop_rtp_mirror, &stream_connection_id); raop_rtp_mirror_start(conn->raop_rtp_mirror, &dport, raop->clientFPSdata); logger_log(raop->logger, LOGGER_DEBUG, "Mirroring initialized successfully"); } else { logger_log(raop->logger, LOGGER_ERR, "Mirroring not initialized at SETUP, playing will fail!"); http_response_set_disconnect(response, 1); } plist_t res_stream_node = plist_new_dict(); plist_t res_stream_data_port_node = plist_new_uint(dport); plist_t res_stream_type_node = plist_new_uint(110); plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node); plist_dict_set_item(res_stream_node, "type", res_stream_type_node); plist_array_append_item(res_streams_node, res_stream_node); break; } case 96: { // Audio unsigned short cport = raop->control_lport, dport = raop->data_lport; unsigned short remote_cport = 0; unsigned char ct = 0; unsigned int sr = AUDIO_SAMPLE_RATE; /* all AirPlay audio formats supported so far have sample rate 44.1kHz */ uint64_t uint_val = 0; plist_t req_stream_control_port_node = plist_dict_get_item(req_stream_node, "controlPort"); plist_get_uint_val(req_stream_control_port_node, &uint_val); remote_cport = (unsigned short) uint_val; /* must != 0 to activate audio resend requests */ plist_t req_stream_ct_node = plist_dict_get_item(req_stream_node, "ct"); plist_get_uint_val(req_stream_ct_node, &uint_val); ct = (unsigned char) uint_val; if (raop->callbacks.audio_get_format) { /* get additional audio format parameters */ uint64_t audioFormat = 0; unsigned short spf = 0; bool isMedia = false; bool usingScreen = false; uint8_t bool_val = 0; plist_t req_stream_spf_node = plist_dict_get_item(req_stream_node, "spf"); plist_get_uint_val(req_stream_spf_node, &uint_val); spf = (unsigned short) uint_val; plist_t req_stream_audio_format_node = plist_dict_get_item(req_stream_node, "audioFormat"); plist_get_uint_val(req_stream_audio_format_node, &audioFormat); plist_t req_stream_is_media_node = plist_dict_get_item(req_stream_node, "isMedia"); if (req_stream_is_media_node) { plist_get_bool_val(req_stream_is_media_node, &bool_val); isMedia = (bool) bool_val; } else { isMedia = false; } plist_t req_stream_using_screen_node = plist_dict_get_item(req_stream_node, "usingScreen"); if (req_stream_using_screen_node) { plist_get_bool_val(req_stream_using_screen_node, &bool_val); usingScreen = (bool) bool_val; } else { usingScreen = false; } raop->callbacks.audio_get_format(raop->callbacks.cls, &ct, &spf, &usingScreen, &isMedia, &audioFormat); } if (conn->raop_rtp) { raop_rtp_start_audio(conn->raop_rtp, &remote_cport, &cport, &dport, &ct, &sr); logger_log(raop->logger, LOGGER_DEBUG, "RAOP initialized success"); } else { logger_log(raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!"); http_response_set_disconnect(response, 1); } plist_t res_stream_node = plist_new_dict(); plist_t res_stream_data_port_node = plist_new_uint(dport); plist_t res_stream_control_port_node = plist_new_uint(cport); plist_t res_stream_type_node = plist_new_uint(96); plist_dict_set_item(res_stream_node, "dataPort", res_stream_data_port_node); plist_dict_set_item(res_stream_node, "controlPort", res_stream_control_port_node); plist_dict_set_item(res_stream_node, "type", res_stream_type_node); plist_array_append_item(res_streams_node, res_stream_node); break; } default: logger_log(raop->logger, LOGGER_ERR, "SETUP tries to setup stream of unknown type %llu", type); http_response_set_disconnect(response, 1); break; } } plist_dict_set_item(res_root_node, "streams", res_streams_node); } plist_to_bin(res_root_node, response_data, (uint32_t*) response_datalen); plist_free(res_root_node); plist_free(req_root_node); http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist"); } static void raop_handler_get_parameter(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const char *content_type = NULL; const char *data = NULL; int datalen = 0; content_type = http_request_get_header(request, "Content-Type"); if (!content_type) { http_response_init(response, "RTSP/1.0", 451, "Parameter not understood"); return; } data = http_request_get_data(request, &datalen); if (!strcmp(content_type, "text/parameters")) { const char *current = data; while (current && (datalen - (current - data) > 0)) { const char *next; /* This is a bit ugly, but seems to be how airport works too */ if ((datalen - (current - data) >= 8) && !strncmp(current, "volume\r\n", 8)) { char volume[25] = "volume: 0.0\r\n"; if (raop->callbacks.audio_set_client_volume) { snprintf(volume, 25, "volume: %9.6f\r\n", raop->callbacks.audio_set_client_volume(raop->callbacks.cls)); } http_response_add_header(response, "Content-Type", "text/parameters"); *response_data = strdup(volume); if (*response_data) { *response_datalen = strlen(*response_data); } return; } for (next = current ; (datalen - (next - data) > 0) ; ++next) { if (*next == '\r') { break; } } if ((datalen - (next - data) >= 2) && !strncmp(next, "\r\n", 2)) { if ((next - current) > 0) { logger_log(raop->logger, LOGGER_WARNING, "Found an unknown parameter: %.*s", (next - current), current); } current = next + 2; } else { current = NULL; } } } } static void raop_handler_set_parameter(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const char *content_type = NULL; const char *data = NULL; int datalen = 0; content_type = http_request_get_header(request, "Content-Type"); if (!content_type) { http_response_init(response, "RTSP/1.0", 451, "Parameter not understood"); return; } data = http_request_get_data(request, &datalen); if (!strcmp(content_type, "text/parameters")) { char *datastr = NULL; datastr = calloc(1, datalen + 1); if (data && datastr && conn->raop_rtp) { memcpy(datastr, data, datalen); if ((datalen >= 8) && !strncmp(datastr, "volume: ", 8)) { float vol = 0.0f; sscanf(datastr+8, "%f", &vol); raop_rtp_set_volume(conn->raop_rtp, vol); } else if ((datalen >= 10) && !strncmp(datastr, "progress: ", 10)) { uint32_t start = 0, curr = 0, end = 0; sscanf(datastr+10, "%"PRIu32"/%"PRIu32"/%"PRIu32, &start, &curr, &end); raop_rtp_set_progress(conn->raop_rtp, start, curr, end); } } else if (!conn->raop_rtp) { logger_log(raop->logger, LOGGER_WARNING, "RAOP not initialized at SET_PARAMETER"); } free(datastr); } else if (!strcmp(content_type, "image/jpeg") || !strcmp(content_type, "image/png")) { logger_log(raop->logger, LOGGER_DEBUG, "Got image data of %d bytes", datalen); if (conn->raop_rtp) { raop_rtp_set_coverart(conn->raop_rtp, data, datalen); } else { logger_log(raop->logger, LOGGER_WARNING, "RAOP not initialized at SET_PARAMETER coverart"); } } else if (!strcmp(content_type, "application/x-dmap-tagged")) { logger_log(raop->logger, LOGGER_DEBUG, "Got metadata of %d bytes", datalen); if (conn->raop_rtp) { raop_rtp_set_metadata(conn->raop_rtp, data, datalen); } else { logger_log(raop->logger, LOGGER_WARNING, "RAOP not initialized at SET_PARAMETER metadata"); } } } static void raop_handler_audiomode(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const char *data = NULL; char *audiomode = NULL; int data_len = 0; data = http_request_get_data(request, &data_len); plist_t req_root_node = NULL; plist_from_bin(data, data_len, &req_root_node); plist_t req_audiomode_node = plist_dict_get_item(req_root_node, "audioMode"); plist_get_string_val(req_audiomode_node, &audiomode); /* not sure what should be done with this request: usually audioMode requested is "default" */ int log_level = (strstr(audiomode, "default") ? LOGGER_DEBUG : LOGGER_INFO); logger_log(raop->logger, log_level, "Unhandled RTSP request \"audioMode: %s\"", audiomode); plist_mem_free(audiomode); plist_free(req_root_node); } static void raop_handler_feedback(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; logger_log(raop->logger, LOGGER_DEBUG, "raop_handler_feedback"); /* register receipt of client's "heartbeat" signal */ raop->callbacks.conn_feedback(raop->callbacks.cls); } static void raop_handler_record(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; char audio_latency[12] = { '\0' }; unsigned int ad = (unsigned int) (((uint64_t) raop->audio_delay_micros) * AUDIO_SAMPLE_RATE / SECOND_IN_USECS); snprintf(audio_latency, sizeof(audio_latency), "%u", ad); logger_log(raop->logger, LOGGER_DEBUG, "raop_handler_record"); http_response_add_header(response, "Audio-Latency", audio_latency); http_response_add_header(response, "Audio-Jack-Status", "connected; type=analog"); } static void raop_handler_flush(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { raop_t *raop = conn->raop; const char *rtpinfo = NULL; int next_seq = -1; rtpinfo = http_request_get_header(request, "RTP-Info"); if (rtpinfo) { logger_log(raop->logger, LOGGER_DEBUG, "Flush with RTP-Info: %s", rtpinfo); if (!strncmp(rtpinfo, "seq=", 4)) { next_seq = strtol(rtpinfo + 4, NULL, 10); } } if (conn->raop_rtp) { raop_rtp_flush(conn->raop_rtp, next_seq); } else { logger_log(raop->logger, LOGGER_WARNING, "RAOP not initialized at FLUSH"); } } static void raop_handler_teardown(raop_conn_t *conn, http_request_t *request, http_response_t *response, char **response_data, int *response_datalen) { /* get the teardown request type(s): (type 96, 110, or none) */ raop_t *raop = conn->raop; const char *data = NULL; int data_len = 0; bool teardown_96 = false, teardown_110 = false; data = http_request_get_data(request, &data_len); plist_t req_root_node = NULL; plist_from_bin(data, data_len, &req_root_node); plist_t req_streams_node = plist_dict_get_item(req_root_node, "streams"); /* Process stream teardown requests */ if (PLIST_IS_ARRAY(req_streams_node)) { uint64_t val = 0; int count = plist_array_get_size(req_streams_node); for (int i = 0; i < count; i++) { plist_t req_stream_node = plist_array_get_item(req_streams_node,i); plist_t req_stream_type_node = plist_dict_get_item(req_stream_node, "type"); plist_get_uint_val(req_stream_type_node, &val); if (val == 96) { teardown_96 = true; } else if (val == 110) { teardown_110 = true; } } } plist_free(req_root_node); logger_log(raop->logger, LOGGER_DEBUG, "TEARDOWN request, 96=%d, 110=%d", teardown_96, teardown_110); http_response_add_header(response, "Connection", "close"); if (teardown_96) { if (conn->raop_rtp) { /* Stop our audio RTP session */ raop_rtp_stop(conn->raop_rtp); /* stop any coverart rendering */ if (raop->callbacks.audio_stop_coverart_rendering) { raop->callbacks.audio_stop_coverart_rendering(raop->callbacks.cls); } } } else if (teardown_110) { if (raop->hls_pending) { raop->callbacks.video_reset(raop->callbacks.cls, RESET_TYPE_RTP_TO_HLS_TEARDOWN); } else { raop->callbacks.video_reset(raop->callbacks.cls, RESET_TYPE_RTP_SHUTDOWN); } if (conn->raop_rtp_mirror) { /* Stop our video RTP session */ raop_rtp_mirror_stop(conn->raop_rtp_mirror); } } else { /* Destroy our sessions */ if (conn->raop_rtp) { raop_rtp_destroy(conn->raop_rtp); conn->raop_rtp = NULL; } if (conn->raop_rtp_mirror) { raop_rtp_mirror_destroy(conn->raop_rtp_mirror); conn->raop_rtp_mirror = NULL; } /* shut down any HLS connections */ int hls_count = httpd_count_connection_type(raop->httpd, CONNECTION_TYPE_HLS); if (hls_count) { raop->callbacks.video_reset(raop->callbacks.cls, RESET_TYPE_HLS_SHUTDOWN); } } }