uxplayer/lib/http_handlers.h
2025-05-03 17:09:23 +08:00

998 lines
39 KiB
C

/**
* Copyright (c) 2024 fduncanh
* All Rights Reserved.
*
* 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.
*
*/
/* this file is part of raop.c and should not be included in any other file */
#include "airplay_video.h"
#include "fcup_request.h"
static void
http_handler_server_info(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
assert(conn->raop->dnssd);
int hw_addr_raw_len = 0;
const char *hw_addr_raw = dnssd_get_hw_addr(conn->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 r_node = plist_new_dict();
/* first 12 AirPlay features bits (R to L): 0x27F = 0010 0111 1111
* Only bits 0-6 and bit 9 are set:
* 0. video supported
* 1. photo supported
* 2. video protected wirh FairPlay DRM
* 3. volume control supported for video
* 4. HLS supported
* 5. slideshow supported
* 6. (unknown)
* 9. audio supported.
*/
plist_t features_node = plist_new_uint(0x27F);
plist_dict_set_item(r_node, "features", features_node);
plist_t mac_address_node = plist_new_string(hw_addr);
plist_dict_set_item(r_node, "macAddress", mac_address_node);
plist_t model_node = plist_new_string(GLOBAL_MODEL);
plist_dict_set_item(r_node, "model", model_node);
plist_t os_build_node = plist_new_string("12B435");
plist_dict_set_item(r_node, "osBuildVersion", os_build_node);
plist_t protovers_node = plist_new_string("1.0");
plist_dict_set_item(r_node, "protovers", protovers_node);
plist_t source_version_node = plist_new_string(GLOBAL_VERSION);
plist_dict_set_item(r_node, "srcvers", source_version_node);
plist_t vv_node = plist_new_uint(strtol(AIRPLAY_VV, NULL, 10));
plist_dict_set_item(r_node, "vv", vv_node);
plist_t device_id_node = plist_new_string(hw_addr);
plist_dict_set_item(r_node, "deviceid", device_id_node);
plist_to_xml(r_node, response_data, (uint32_t *) response_datalen);
//assert(*response_datalen == strlen(*response_data));
/* last character (at *response_data[response_datalen - 1]) is 0x0a = '\n'
* (*response_data[response_datalen] is '\0').
* apsdk removes the last "\n" by overwriting it with '\0', and reducing response_datalen by 1.
* TODO: check if this is necessary */
plist_free(r_node);
http_response_add_header(response, "Content-Type", "text/x-apple-plist+xml");
free(hw_addr);
/* initialize the airplay video service */
const char *session_id = http_request_get_header(request, "X-Apple-Session-ID");
airplay_video_service_init(conn->raop, conn->raop->port, session_id);
}
static void
http_handler_scrub(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *data = strstr(url, "?");
float scrub_position = 0.0f;
if (data) {
data++;
const char *position = strstr(data, "=") + 1;
char *end;
double value = strtod(position, &end);
if (end && end != position) {
scrub_position = (float) value;
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_scrub: got position = %.6f",
scrub_position);
}
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "**********************SCRUB %f ***********************",scrub_position);
conn->raop->callbacks.on_video_scrub(conn->raop->callbacks.cls, scrub_position);
}
static void
http_handler_rate(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *data = strstr(url, "?");
float rate_value = 0.0f;
if (data) {
data++;
const char *rate = strstr(data, "=") + 1;
char *end;
float value = strtof(rate, &end);
if (end && end != rate) {
rate_value = value;
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_rate: got rate = %.6f", rate_value);
}
}
conn->raop->callbacks.on_video_rate(conn->raop->callbacks.cls, rate_value);
}
static void
http_handler_stop(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
logger_log(conn->raop->logger, LOGGER_INFO, "client HTTP request POST stop");
conn->raop->callbacks.on_video_stop(conn->raop->callbacks.cls);
}
/* handles PUT /setProperty http requests from Client to Server */
static void
http_handler_set_property(raop_conn_t *conn,
http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *property = url + strlen("/setProperty?");
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_set_property: %s", property);
/* actionAtItemEnd: values:
0: advance (advance to next item, if there is one)
1: pause (pause playing)
2: none (do nothing)
reverseEndTime (only used when rate < 0) time at which reverse playback ends
forwardEndTime (only used when rate > 0) time at which reverse playback ends
*/
if (!strcmp(property, "reverseEndTime") ||
!strcmp(property, "forwardEndTime") ||
!strcmp(property, "actionAtItemEnd")) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "property %s is known but unhandled", property);
plist_t errResponse = plist_new_dict();
plist_t errCode = plist_new_uint(0);
plist_dict_set_item(errResponse, "errorCode", errCode);
plist_to_xml(errResponse, response_data, (uint32_t *) response_datalen);
plist_free(errResponse);
http_response_add_header(response, "Content-Type", "text/x-apple-plist+xml");
} else {
logger_log(conn->raop->logger, LOGGER_DEBUG, "property %s is unknown, unhandled", property);
http_response_add_header(response, "Content-Length", "0");
}
}
/* handles GET /getProperty http requests from Client to Server. (not implemented) */
static void
http_handler_get_property(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
const char *url = http_request_get_url(request);
const char *property = url + strlen("getProperty?");
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_get_property: %s (unhandled)", property);
}
/* this request (for a variant FairPlay decryption) cannot be handled by UxPlay */
static void
http_handler_fpsetup2(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
logger_log(conn->raop->logger, LOGGER_WARNING, "client HTTP request POST fp-setup2 is unhandled");
http_response_add_header(response, "Content-Type", "application/x-apple-binary-plist");
int req_datalen;
const unsigned char *req_data = (unsigned char *) http_request_get_data(request, &req_datalen);
logger_log(conn->raop->logger, LOGGER_ERR, "only FairPlay version 0x03 is implemented, version is 0x%2.2x",
req_data[4]);
http_response_init(response, "HTTP/1.1", 421, "Misdirected Request");
}
// called by http_handler_playback_info while preparing response to a GET /playback_info request from the client.
typedef struct time_range_s {
double start;
double duration;
} time_range_t;
void time_range_to_plist(void *time_ranges, const int n_time_ranges,
plist_t time_ranges_node) {
time_range_t *tr = (time_range_t *) time_ranges;
for (int i = 0 ; i < n_time_ranges; i++) {
plist_t time_range_node = plist_new_dict();
plist_t duration_node = plist_new_real(tr[i].duration);
plist_dict_set_item(time_range_node, "duration", duration_node);
plist_t start_node = plist_new_real(tr[i].start);
plist_dict_set_item(time_range_node, "start", start_node);
plist_array_append_item(time_ranges_node, time_range_node);
}
}
// called by http_handler_playback_info while preparing response to a GET /playback_info request from the client.
int create_playback_info_plist_xml(playback_info_t *playback_info, char **plist_xml) {
plist_t res_root_node = plist_new_dict();
plist_t duration_node = plist_new_real(playback_info->duration);
plist_dict_set_item(res_root_node, "duration", duration_node);
plist_t position_node = plist_new_real(playback_info->position);
plist_dict_set_item(res_root_node, "position", position_node);
plist_t rate_node = plist_new_real(playback_info->rate);
plist_dict_set_item(res_root_node, "rate", rate_node);
/* should these be int or bool? */
plist_t ready_to_play_node = plist_new_uint(playback_info->ready_to_play);
plist_dict_set_item(res_root_node, "readyToPlay", ready_to_play_node);
plist_t playback_buffer_empty_node = plist_new_uint(playback_info->playback_buffer_empty);
plist_dict_set_item(res_root_node, "playbackBufferEmpty", playback_buffer_empty_node);
plist_t playback_buffer_full_node = plist_new_uint(playback_info->playback_buffer_full);
plist_dict_set_item(res_root_node, "playbackBufferFull", playback_buffer_full_node);
plist_t playback_likely_to_keep_up_node = plist_new_uint(playback_info->playback_likely_to_keep_up);
plist_dict_set_item(res_root_node, "playbackLikelyToKeepUp", playback_likely_to_keep_up_node);
plist_t loaded_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->loadedTimeRanges, playback_info->num_loaded_time_ranges,
loaded_time_ranges_node);
plist_dict_set_item(res_root_node, "loadedTimeRanges", loaded_time_ranges_node);
plist_t seekable_time_ranges_node = plist_new_array();
time_range_to_plist(playback_info->seekableTimeRanges, playback_info->num_seekable_time_ranges,
seekable_time_ranges_node);
plist_dict_set_item(res_root_node, "seekableTimeRanges", seekable_time_ranges_node);
int len;
plist_to_xml(res_root_node, plist_xml, (uint32_t *) &len);
/* plist_xml is null-terminated, last character is '/n' */
plist_free(res_root_node);
return len;
}
/* this handles requests from the Client for "Playback information" while the Media is playing on the
Media Player. (The Server gets this information by monitoring the Media Player). The Client could use
the information to e.g. update the slider it shows with progress to the player (0%-100%).
It does not affect playing of the Media*/
static void
http_handler_playback_info(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen)
{
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_playback_info");
//const char *session_id = http_request_get_header(request, "X-Apple-Session-ID");
playback_info_t playback_info;
playback_info.stallcount = 0;
playback_info.ready_to_play = true; // ???;
playback_info.playback_buffer_empty = false; // maybe need to get this from playbin
playback_info.playback_buffer_full = true;
playback_info.playback_likely_to_keep_up = true;
conn->raop->callbacks.on_video_acquire_playback_info(conn->raop->callbacks.cls, &playback_info);
if (playback_info.duration == -1.0) {
/* video has finished, reset */
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available (finishing)");
//httpd_remove_known_connections(conn->raop->httpd);
http_response_set_disconnect(response,1);
conn->raop->callbacks.video_reset(conn->raop->callbacks.cls);
return;
} else if (playback_info.position == -1.0) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "playback_info not available");
return;
}
playback_info.num_loaded_time_ranges = 1;
time_range_t time_ranges_loaded[1];
time_ranges_loaded[0].start = playback_info.position;
time_ranges_loaded[0].duration = playback_info.duration - playback_info.position;
playback_info.loadedTimeRanges = (void *) &time_ranges_loaded;
playback_info.num_seekable_time_ranges = 1;
time_range_t time_ranges_seekable[1];
time_ranges_seekable[0].start = 0.0;
time_ranges_seekable[0].duration = playback_info.position;
playback_info.seekableTimeRanges = (void *) &time_ranges_seekable;
*response_datalen = create_playback_info_plist_xml(&playback_info, response_data);
http_response_add_header(response, "Content-Type", "text/x-apple-plist+xml");
}
/* this handles the POST /reverse request from Client to Server on a AirPlay http channel to "Upgrade"
to "PTTH/1.0" Reverse HTTP protocol proposed in 2009 Internet-Draft
https://datatracker.ietf.org/doc/id/draft-lentczner-rhttp-00.txt .
After the Upgrade the channel becomes a reverse http "AirPlay (reversed)" channel for
http requests from Server to Client.
*/
static void
http_handler_reverse(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
/* get http socket for send */
int socket_fd = httpd_get_connection_socket (conn->raop->httpd, (void *) conn);
if (socket_fd < 0) {
logger_log(conn->raop->logger, LOGGER_ERR, "fcup_request failed to retrieve socket_fd from httpd");
/* shut down connection? */
}
const char *purpose = http_request_get_header(request, "X-Apple-Purpose");
const char *connection = http_request_get_header(request, "Connection");
const char *upgrade = http_request_get_header(request, "Upgrade");
logger_log(conn->raop->logger, LOGGER_INFO, "client requested reverse connection: %s; purpose: %s \"%s\"",
connection, upgrade, purpose);
httpd_set_connection_type(conn->raop->httpd, (void *) conn, CONNECTION_TYPE_PTTH);
int type_PTTH = httpd_count_connection_type(conn->raop->httpd, CONNECTION_TYPE_PTTH);
if (type_PTTH == 1) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "will use socket %d for %s connections", socket_fd, purpose);
http_response_init(response, "HTTP/1.1", 101, "Switching Protocols");
http_response_add_header(response, "Connection", "Upgrade");
http_response_add_header(response, "Upgrade", "PTTH/1.0");
} else {
logger_log(conn->raop->logger, LOGGER_ERR, "multiple TPPH connections (%d) are forbidden", type_PTTH );
}
}
/* this copies a Media Playlist into a null-terminated string. If it has the "#YT-EXT-CONDENSED-URI"
header, it is also expanded into the full Media Playlist format */
char *adjust_yt_condensed_playlist(const char *media_playlist) {
/* expands a YT-EXT_CONDENSED-URL media playlist into a full media playlist
* returns a pointer to the expanded playlist, WHICH MUST BE FREED AFTER USE */
const char *base_uri_begin;
const char *params_begin;
const char *prefix_begin;
size_t base_uri_len;
size_t params_len;
size_t prefix_len;
const char* ptr = strstr(media_playlist, "#EXTM3U\n");
ptr += strlen("#EXTM3U\n");
assert(ptr);
if (strncmp(ptr, "#YT-EXT-CONDENSED-URL", strlen("#YT-EXT-CONDENSED-URL"))) {
size_t len = strlen(media_playlist);
char * playlist_copy = (char *) malloc(len + 1);
memcpy(playlist_copy, media_playlist, len);
playlist_copy[len] = '\0';
return playlist_copy;
}
ptr = strstr(ptr, "BASE-URI=");
base_uri_begin = strchr(ptr, '"');
base_uri_begin++;
ptr = strchr(base_uri_begin, '"');
base_uri_len = ptr - base_uri_begin;
char *base_uri = (char *) calloc(base_uri_len + 1, sizeof(char));
assert(base_uri);
memcpy(base_uri, base_uri_begin, base_uri_len); //must free
ptr = strstr(ptr, "PARAMS=");
params_begin = strchr(ptr, '"');
params_begin++;
ptr = strchr(params_begin,'"');
params_len = ptr - params_begin;
char *params = (char *) calloc(params_len + 1, sizeof(char));
assert(params);
memcpy(params, params_begin, params_len); //must free
ptr = strstr(ptr, "PREFIX=");
prefix_begin = strchr(ptr, '"');
prefix_begin++;
ptr = strchr(prefix_begin,'"');
prefix_len = ptr - prefix_begin;
char *prefix = (char *) calloc(prefix_len + 1, sizeof(char));
assert(prefix);
memcpy(prefix, prefix_begin, prefix_len); //must free
/* expand params */
int nparams = 0;
int *params_size = NULL;
const char **params_start = NULL;
if (strlen(params)) {
nparams = 1;
char * comma = strchr(params, ',');
while (comma) {
nparams++;
comma++;
comma = strchr(comma, ',');
}
params_start = (const char **) calloc(nparams, sizeof(char *)); //must free
params_size = (int *) calloc(nparams, sizeof(int)); //must free
ptr = params;
for (int i = 0; i < nparams; i++) {
comma = strchr(ptr, ',');
params_start[i] = ptr;
if (comma) {
params_size[i] = (int) (comma - ptr);
ptr = comma;
ptr++;
} else {
params_size[i] = (int) (params + params_len - ptr);
break;
}
}
}
int count = 0;
ptr = strstr(media_playlist, "#EXTINF");
while (ptr) {
count++;
ptr = strstr(++ptr, "#EXTINF");
}
size_t old_size = strlen(media_playlist);
size_t new_size = old_size;
new_size += count * (base_uri_len + params_len);
char * new_playlist = (char *) calloc( new_size + 100, sizeof(char));
const char *old_pos = media_playlist;
char *new_pos = new_playlist;
ptr = old_pos;
ptr = strstr(old_pos, "#EXTINF:");
size_t len = ptr - old_pos;
/* copy header section before chunks */
memcpy(new_pos, old_pos, len);
old_pos += len;
new_pos += len;
int counter = 0;
while (ptr) {
counter++;
/* for each chunk */
const char *end = NULL;
char *start = strstr(ptr, prefix);
len = start - ptr;
/* copy first line of chunk entry */
memcpy(new_pos, old_pos, len);
old_pos += len;
new_pos += len;
/* copy base uri to replace prefix*/
memcpy(new_pos, base_uri, base_uri_len);
new_pos += base_uri_len;
old_pos += prefix_len;
ptr = strstr(old_pos, "#EXTINF:");
/* insert the PARAMS separators on the slices line */
end = old_pos;
int last = nparams - 1;
for (int i = 0; i < nparams; i++) {
if (i != last) {
end = strchr(end, '/');
} else {
end = strstr(end, "#EXT"); /* the next line starts with either #EXTINF (usually) or #EXT-X-ENDLIST (at last chunk)*/
}
*new_pos = '/';
new_pos++;
memcpy(new_pos, params_start[i], params_size[i]);
new_pos += params_size[i];
*new_pos = '/';
new_pos++;
len = end - old_pos;
end++;
memcpy (new_pos, old_pos, len);
new_pos += len;
old_pos += len;
if (i != last) {
old_pos++; /* last entry is not followed by "/" separator */
}
}
}
/* copy tail */
len = media_playlist + strlen(media_playlist) - old_pos;
memcpy(new_pos, old_pos, len);
new_pos += len;
old_pos += len;
new_playlist[new_size] = '\0';
free (prefix);
free (base_uri);
free (params);
if (params_size) {
free (params_size);
}
if (params_start) {
free (params_start);
}
return new_playlist;
}
/* this adjusts the uri prefixes in the Master Playlist, for sending to the Media Player running on the Server Host */
char *adjust_master_playlist (char *fcup_response_data, int fcup_response_datalen, char *uri_prefix, char *uri_local_prefix) {
size_t uri_prefix_len = strlen(uri_prefix);
size_t uri_local_prefix_len = strlen(uri_local_prefix);
int counter = 0;
char *ptr = strstr(fcup_response_data, uri_prefix);
while (ptr != NULL) {
counter++;
ptr++;
ptr = strstr(ptr, uri_prefix);
}
size_t len = uri_local_prefix_len - uri_prefix_len;
len *= counter;
len += fcup_response_datalen;
char *new_master = (char *) malloc(len + 1);
*(new_master + len) = '\0';
char *first = fcup_response_data;
char *new = new_master;
char *last = strstr(first, uri_prefix);
counter = 0;
while (last != NULL) {
counter++;
len = last - first;
memcpy(new, first, len);
first = last + uri_prefix_len;
new += len;
memcpy(new, uri_local_prefix, uri_local_prefix_len);
new += uri_local_prefix_len;
last = strstr(last + uri_prefix_len, uri_prefix);
if (last == NULL) {
len = fcup_response_data + fcup_response_datalen - first;
memcpy(new, first, len);
break;
}
}
return new_master;
}
/* this parses the Master Playlist to make a table of the Media Playlist uri's that it lists */
int create_media_uri_table(const char *url_prefix, const char *master_playlist_data, int datalen,
char ***media_uri_table, int *num_uri) {
char *ptr = strstr(master_playlist_data, url_prefix);
char ** table = NULL;
if (ptr == NULL) {
return -1;
}
int count = 0;
while (ptr != NULL) {
char *end = strstr(ptr, "m3u8");
if (end == NULL) {
return 1;
}
end += sizeof("m3u8");
count++;
ptr = strstr(end, url_prefix);
}
table = (char **) calloc(count, sizeof(char *));
if (!table) {
return -1;
}
for (int i = 0; i < count; i++) {
table[i] = NULL;
}
ptr = strstr(master_playlist_data, url_prefix);
count = 0;
while (ptr != NULL) {
char *end = strstr(ptr, "m3u8");
char *uri;
if (end == NULL) {
return 0;
}
end += sizeof("m3u8");
size_t len = end - ptr - 1;
uri = (char *) calloc(len + 1, sizeof(char));
memcpy(uri , ptr, len);
table[count] = uri;
uri = NULL;
count ++;
ptr = strstr(end, url_prefix);
}
*num_uri = count;
*media_uri_table = table;
return 0;
}
/* the POST /action request from Client to Server on the AirPlay http channel follows a POST /event "FCUP Request"
from Server to Client on the reverse http channel, for a HLS playlist (first the Master Playlist, then the Media Playlists
listed in the Master Playlist. The POST /action request contains the playlist requested by the Server in
the preceding "FCUP Request". The FCUP Request sequence continues until all Media Playlists have been obtained by the Server */
static void
http_handler_action(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
bool data_is_plist = false;
plist_t req_root_node = NULL;
uint64_t uint_val;
int request_id = 0;
int fcup_response_statuscode = 0;
bool logger_debug = (logger_get_level(conn->raop->logger) >= LOGGER_DEBUG);
const char* session_id = http_request_get_header(request, "X-Apple-Session-ID");
if (!session_id) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request had no X-Apple-Session-ID");
goto post_action_error;
}
const char *apple_session_id = get_apple_session_id(conn->raop->airplay_video);
if (strcmp(session_id, apple_session_id)){
logger_log(conn->raop->logger, LOGGER_ERR, "X-Apple-Session-ID has changed:\n was:\"%s\"\n now:\"%s\"",
apple_session_id, session_id);
goto post_action_error;
}
/* verify that this request contains a binary plist*/
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
logger_log(conn->raop->logger, LOGGER_DEBUG, "request header: %s", header_str);
data_is_plist = (strstr(header_str,"apple-binary-plist") != NULL);
free(header_str);
if (!data_is_plist) {
logger_log(conn->raop->logger, LOGGER_INFO, "POST /action: did not receive expected plist from client");
goto post_action_error;
}
/* extract the root_node plist */
int request_datalen = 0;
const char *request_data = http_request_get_data(request, &request_datalen);
if (request_datalen == 0) {
logger_log(conn->raop->logger, LOGGER_INFO, "POST /action: did not receive expected plist from client");
goto post_action_error;
}
plist_from_bin(request_data, request_datalen, &req_root_node);
/* determine type of data */
plist_t req_type_node = plist_dict_get_item(req_root_node, "type");
if (!PLIST_IS_STRING(req_type_node)) {
goto post_action_error;
}
/* three possible types are known */
char *type = NULL;
int action_type = 0;
plist_get_string_val(req_type_node, &type);
logger_log(conn->raop->logger, LOGGER_DEBUG, "action type is %s", type);
if (strstr(type, "unhandledURLResponse")) {
action_type = 1;
} else if (strstr(type, "playlistInsert")) {
action_type = 2;
} else if (strstr(type, "playlistRemove")) {
action_type = 3;
}
free (type);
plist_t req_params_node = NULL;
switch (action_type) {
case 1:
goto unhandledURLResponse;
case 2:
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistInsert (add new playback)");
goto finish;
case 3:
logger_log(conn->raop->logger, LOGGER_INFO, "unhandled action type playlistRemove (stop playback)");
goto finish;
default:
logger_log(conn->raop->logger, LOGGER_INFO, "unknown action type (unhandled)");
goto finish;
}
unhandledURLResponse:;
req_params_node = plist_dict_get_item(req_root_node, "params");
if (!PLIST_IS_DICT (req_params_node)) {
goto post_action_error;
}
/* handling type "unhandledURLResponse" (case 1)*/
uint_val = 0;
int fcup_response_datalen = 0;
if (logger_debug) {
plist_t plist_fcup_response_statuscode_node = plist_dict_get_item(req_params_node,
"FCUP_Response_StatusCode");
if (plist_fcup_response_statuscode_node) {
plist_get_uint_val(plist_fcup_response_statuscode_node, &uint_val);
fcup_response_statuscode = (int) uint_val;
uint_val = 0;
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response_StatusCode = %d",
fcup_response_statuscode);
}
plist_t plist_fcup_response_requestid_node = plist_dict_get_item(req_params_node,
"FCUP_Response_RequestID");
if (plist_fcup_response_requestid_node) {
plist_get_uint_val(plist_fcup_response_requestid_node, &uint_val);
request_id = (int) uint_val;
uint_val = 0;
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response_RequestID = %d", request_id);
}
}
plist_t plist_fcup_response_url_node = plist_dict_get_item(req_params_node, "FCUP_Response_URL");
if (!PLIST_IS_STRING(plist_fcup_response_url_node)) {
goto post_action_error;
}
char *fcup_response_url = NULL;
plist_get_string_val(plist_fcup_response_url_node, &fcup_response_url);
if (!fcup_response_url) {
goto post_action_error;
}
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response_URL = %s", fcup_response_url);
plist_t plist_fcup_response_data_node = plist_dict_get_item(req_params_node, "FCUP_Response_Data");
if (!PLIST_IS_DATA(plist_fcup_response_data_node)){
goto post_action_error;
}
uint_val = 0;
char *fcup_response_data = NULL;
plist_get_data_val(plist_fcup_response_data_node, &fcup_response_data, &uint_val);
fcup_response_datalen = (int) uint_val;
if (!fcup_response_data) {
free (fcup_response_url);
goto post_action_error;
}
if (logger_debug) {
logger_log(conn->raop->logger, LOGGER_DEBUG, "FCUP_Response datalen = %d", fcup_response_datalen);
char *data = malloc(fcup_response_datalen + 1);
memcpy(data, fcup_response_data, fcup_response_datalen);
data[fcup_response_datalen] = '\0';
logger_log(conn->raop->logger, LOGGER_DEBUG, "begin FCUP Response data:\n%s\nend FCUP Response data",data);
free (data);
}
char *ptr = strstr(fcup_response_url, "/master.m3u8");
if (ptr) {
/* this is a master playlist */
char *uri_prefix = get_uri_prefix(conn->raop->airplay_video);
char ** media_data_store = NULL;
int num_uri = 0;
char *uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
char *new_master = adjust_master_playlist (fcup_response_data, fcup_response_datalen, uri_prefix, uri_local_prefix);
store_master_playlist(conn->raop->airplay_video, new_master);
create_media_uri_table(uri_prefix, fcup_response_data, fcup_response_datalen, &media_data_store, &num_uri);
create_media_data_store(conn->raop->airplay_video, media_data_store, num_uri);
num_uri = get_num_media_uri(conn->raop->airplay_video);
set_next_media_uri_id(conn->raop->airplay_video, 0);
} else {
/* this is a media playlist */
assert(fcup_response_data);
char *playlist = (char *) calloc(fcup_response_datalen + 1, sizeof(char));
memcpy(playlist, fcup_response_data, fcup_response_datalen);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
--uri_num; // (next num is current num + 1)
store_media_data_playlist_by_num(conn->raop->airplay_video, playlist, uri_num);
float duration = 0.0f;
int count = analyze_media_playlist(playlist, &duration);
if (count) {
logger_log(conn->raop->logger, LOGGER_DEBUG,
"\n%s:\nreceived media playlist has %5d chunks, total duration %9.3f secs\n",
fcup_response_url, count, duration);
}
}
if (fcup_response_data) {
free (fcup_response_data);
}
if (fcup_response_url) {
free (fcup_response_url);
}
int num_uri = get_num_media_uri(conn->raop->airplay_video);
int uri_num = get_next_media_uri_id(conn->raop->airplay_video);
if (uri_num < num_uri) {
fcup_request((void *) conn, get_media_uri_by_num(conn->raop->airplay_video, uri_num),
apple_session_id,
get_next_FCUP_RequestID(conn->raop->airplay_video));
set_next_media_uri_id(conn->raop->airplay_video, ++uri_num);
} else {
char * uri_local_prefix = get_uri_local_prefix(conn->raop->airplay_video);
conn->raop->callbacks.on_video_play(conn->raop->callbacks.cls,
strcat(uri_local_prefix, "/master.m3u8"),
get_start_position_seconds(conn->raop->airplay_video));
}
finish:
plist_free(req_root_node);
return;
post_action_error:;
http_response_init(response, "HTTP/1.1", 400, "Bad Request");
if (req_root_node) {
plist_free(req_root_node);
}
}
/* The POST /play request from the Client to Server on the AirPlay http channel contains (among other information)
the "Content Location" that specifies the HLS Playlists for the video to be streamed, as well as the video
"start position in seconds". Once this request is received by the Sever, the Server sends a POST /event
"FCUP Request" request to the Client on the reverse http channel, to request the HLS Master Playlist */
static void
http_handler_play(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
char* playback_location = NULL;
plist_t req_root_node = NULL;
float start_position_seconds = 0.0f;
bool data_is_binary_plist = false;
bool data_is_text = false;
bool data_is_octet = false;
logger_log(conn->raop->logger, LOGGER_DEBUG, "http_handler_play");
const char* session_id = http_request_get_header(request, "X-Apple-Session-ID");
if (!session_id) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request had no X-Apple-Session-ID");
goto play_error;
}
const char *apple_session_id = get_apple_session_id(conn->raop->airplay_video);
if (strcmp(session_id, apple_session_id)){
logger_log(conn->raop->logger, LOGGER_ERR, "X-Apple-Session-ID has changed:\n was:\"%s\"\n now:\"%s\"",
apple_session_id, session_id);
goto play_error;
}
int request_datalen = -1;
const char *request_data = http_request_get_data(request, &request_datalen);
if (request_datalen > 0) {
char *header_str = NULL;
http_request_get_header_string(request, &header_str);
logger_log(conn->raop->logger, LOGGER_DEBUG, "request header:\n%s", header_str);
data_is_binary_plist = (strstr(header_str, "x-apple-binary-plist") != NULL);
data_is_text = (strstr(header_str, "text/parameters") != NULL);
data_is_octet = (strstr(header_str, "octet-stream") != NULL);
free (header_str);
}
if (!data_is_text && !data_is_octet && !data_is_binary_plist) {
goto play_error;
}
if (data_is_text) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is text (unsupported)");
goto play_error;
}
if (data_is_octet) {
logger_log(conn->raop->logger, LOGGER_ERR, "Play request Content is octet-stream (unsupported)");
goto play_error;
}
if (data_is_binary_plist) {
plist_from_bin(request_data, request_datalen, &req_root_node);
plist_t req_uuid_node = plist_dict_get_item(req_root_node, "uuid");
if (!req_uuid_node) {
goto play_error;
} else {
char* playback_uuid = NULL;
plist_get_string_val(req_uuid_node, &playback_uuid);
set_playback_uuid(conn->raop->airplay_video, playback_uuid);
free (playback_uuid);
}
plist_t req_content_location_node = plist_dict_get_item(req_root_node, "Content-Location");
if (!req_content_location_node) {
goto play_error;
} else {
plist_get_string_val(req_content_location_node, &playback_location);
}
plist_t req_start_position_seconds_node = plist_dict_get_item(req_root_node, "Start-Position-Seconds");
if (!req_start_position_seconds_node) {
logger_log(conn->raop->logger, LOGGER_INFO, "No Start-Position-Seconds in Play request");
} else {
double start_position = 0.0;
plist_get_real_val(req_start_position_seconds_node, &start_position);
start_position_seconds = (float) start_position;
}
set_start_position_seconds(conn->raop->airplay_video, (float) start_position_seconds);
}
char *ptr = strstr(playback_location, "/master.m3u8");
int prefix_len = (int) (ptr - playback_location);
set_uri_prefix(conn->raop->airplay_video, playback_location, prefix_len);
set_next_media_uri_id(conn->raop->airplay_video, 0);
fcup_request((void *) conn, playback_location, apple_session_id, get_next_FCUP_RequestID(conn->raop->airplay_video));
if (playback_location) {
free (playback_location);
}
if (req_root_node) {
plist_free(req_root_node);
}
return;
play_error:;
if (req_root_node) {
plist_free(req_root_node);
}
logger_log(conn->raop->logger, LOGGER_ERR, "Could not find valid Plist Data for /play, Unhandled");
http_response_init(response, "HTTP/1.1", 400, "Bad Request");
}
/* the HLS handler handles http requests GET /[uri] on the HLS channel from the media player to the Server, asking for
(adjusted) copies of Playlists: first the Master Playlist (adjusted to change the uri prefix to
"http://localhost:[port]/.......m3u8"), then the Media Playlists that the media player wishes to use.
If the client supplied Media playlists with the "YT-EXT-CONDENSED-URI" header, these must be adjusted into
the standard uncondensed form before sending with the response. The uri in the request is the uri for the
Media Playlist, taken from the Master Playlist, with the uri prefix removed.
*/
static void
http_handler_hls(raop_conn_t *conn, http_request_t *request, http_response_t *response,
char **response_data, int *response_datalen) {
const char *method = http_request_get_method(request);
assert (!strcmp(method, "GET"));
const char *url = http_request_get_url(request);
const char* upgrade = http_request_get_header(request, "Upgrade");
if (upgrade) {
//don't accept Upgrade: h2c request ?
return;
}
if (!strcmp(url, "/master.m3u8")){
char * master_playlist = get_master_playlist(conn->raop->airplay_video);
size_t len = strlen(master_playlist);
char * data = (char *) malloc(len + 1);
memcpy(data, master_playlist, len);
data[len] = '\0';
*response_data = data;
*response_datalen = (int ) len;
} else {
int num = get_media_playlist_by_uri(conn->raop->airplay_video, url);
if (num < 0) {
logger_log(conn->raop->logger, LOGGER_ERR,"Requested playlist %s not found", url);
assert(0);
} else {
char *media_playlist = get_media_playlist_by_num(conn->raop->airplay_video, num);
assert(media_playlist);
char *data = adjust_yt_condensed_playlist(media_playlist);
*response_data = data;
*response_datalen = strlen(data);
float duration = 0.0f;
int chunks = analyze_media_playlist(data, &duration);
logger_log(conn->raop->logger, LOGGER_INFO,
"Requested media_playlist %s has %5d chunks, total duration %9.3f secs", url, chunks, duration);
}
}
http_response_add_header(response, "Access-Control-Allow-Headers", "Content-type");
http_response_add_header(response, "Access-Control-Allow-Origin", "*");
const char *date;
date = gmt_time_string();
http_response_add_header(response, "Date", date);
if (*response_datalen > 0) {
http_response_add_header(response, "Content-Type", "application/x-mpegURL; charset=utf-8");
} else if (*response_datalen == 0) {
http_response_init(response, "HTTP/1.1", 404, "Not Found");
}
}