From 26fcbf35fe3a9eb25f17f4929f7f0e2076ed3947 Mon Sep 17 00:00:00 2001 From: "E.Smith" <31170571+azlm8t@users.noreply.github.com> Date: Wed, 10 Jun 2020 18:45:04 +0100 Subject: [PATCH] hdhomerun: Add HDHomeRun server support for LiveTV only (#4461) We now offer the ability to pretend to be an HDHomeRun server to expose channels for LiveTV. DVR functionality is not exposed. This only works on media players that allow IP:Port number configuration. It is hooked in to the webui and only exists if the feature is compiled in and explicitly enabled in the GUI. To use: - Add a user and enable persistent authentication via Config->User->Passwords->Persistent Authentication Enable. - Use this user for the HDHomeRun server setup in Config->General->Base->HDHomeRun/Local Username. Then manually add the device to your client using the IP address and port of the TVHeadend server, such as 192.168.0.1:9981 Auto-discovery of the TVHeadend server by downstream media clients does not work since clients seem to use different mechanisms. So, although many clients (such as TVHeadend) use a broadcast at 65001 to locate clients, one of the popular media players does not use this mechanism. The persistent authentication allows the client to stream LiveTV from the TVHeadend server using the credentials for the given user. Using a user allows us to do filtering such as only allowing specific channels to be passed through to the downstream clients. For "DeviceAuth" we use a (hard-coded) randomly generated value since it has to be 24 lowercase/digit characters long. For DeviceID, we base it on the server name. Technically a DeviceID needs to match a particular format to validate against a CRC, but no clients seems to enforce this. The feature is configurable. If the feature is disabled then we hide the options in the GUI rather than compile them out completely. This ensures that if the user accidentally compiles with the options disabled then they will not lose their configuration. Issue: #4461 --- configure | 1 + .../config_hdhomerun_server_username.md | 23 ++ src/config.c | 88 +++++ src/config.h | 4 + src/htsmsg.c | 58 ++++ src/htsmsg.h | 7 + src/htsmsg_json.h | 1 + src/webui/webui.c | 309 ++++++++++++++++++ 8 files changed, 491 insertions(+) create mode 100644 docs/property/config_hdhomerun_server_username.md diff --git a/configure b/configure index e76760479..7b400f881 100755 --- a/configure +++ b/configure @@ -28,6 +28,7 @@ OPTIONS=( "satip_server:yes" "satip_client:yes" "hdhomerun_client:no" + "hdhomerun_server:yes" "hdhomerun_static:yes" "iptv:yes" "tsfile:yes" diff --git a/docs/property/config_hdhomerun_server_username.md b/docs/property/config_hdhomerun_server_username.md new file mode 100644 index 000000000..53c1246ec --- /dev/null +++ b/docs/property/config_hdhomerun_server_username.md @@ -0,0 +1,23 @@ +Tvheadend can pretend to be an HDHomeRun device. +This is only available if Tvheadend has been compiled +with the feature enabled. + +This is currently an experimental feature. It may +be withdrawn in the future. + +This allows some media players to be able to access +Live TV channels. Some other media players use a +different technique to access HDHomeRun so will not +be able to play Live TV channels. + +On the media player, autodetect of devices will not work. +Instead, you need to enter the IP address and port +number of the Tvheadend web interface. For example +```1.2.3.4:9981```. If the media player does not +support manually entering details then it is not +supported. + +Typically the media player will then ask for xmltv +details to populate the TV Guide. This can normally +be retrieved from Tvheadend via the web interface. +For example ```http://1.2.3.4:9981/xmltv/channels```. diff --git a/src/config.c b/src/config.c index 2293d278b..c6ba81caf 100644 --- a/src/config.c +++ b/src/config.c @@ -2085,6 +2085,7 @@ PROP_DOC(config_channelicon_path) PROP_DOC(config_channelname_scheme) PROP_DOC(config_picon_path) PROP_DOC(config_picon_servicetype) +PROP_DOC(config_hdhomerun_server_username) PROP_DOC(viewlevel_config) PROP_DOC(themes) @@ -2507,6 +2508,93 @@ const idclass_t config_class = { .opts = PO_HIDDEN | PO_EXPERT, .group = 6 }, + { + .type = PT_STR, + .id = "hdhomerun_server_username", + .name = N_("Tvheadend username for HDHomeRun Server Emulation"), + .desc = N_("When Tvheadend is acting as an HDHomeRun Server " + "(emulating an HDHomeRun device for downstream " + "media devices to stream Live TV) then " + "we use this user for determining permissions " + "such as channels that can be streamed. " + "This user must have basic streaming permissions. " + "This user must also have persistent " + "authentication enabled in the user password settings. " + "It is strongly recommended that IP Blocking is used to " + "prevent access from outside your network." + ), + .doc = prop_doc_config_hdhomerun_server_username, + .off = offsetof(config_t, hdhomerun_server_username), + .opts = PO_EXPERT +#if !ENABLE_HDHOMERUN_SERVER + | PO_PHIDDEN +#endif + , + .group = 6, + }, + { + .type = PT_U32, + .id = "hdhomerun_server_tuner_count", + .name = N_("Number of tuners to export for HDHomeRun Server Emulation"), + .desc = N_("When Tvheadend is acting as an HDHomeRun Server " + "(emulating an HDHomeRun device for downstream " + "media devices to stream Live TV) then " + "we tell clients that we have this number of tuners. " + "This is necessary since some clients artificially limit " + "connections based on tuner count, even though several " + "channels may share a multiplex on one tuner. " + "The HDHomeRun interface can not distinguish between " + "different types of tuner in a mixed system with " + "satellite, aerial and cable. " + "The actual number or types of tuners used by Tvheadend is " + "not affected by this value. Tvheadend will " + "allocate tuners automatically. " + "Set to zero for Tvheadend to use a default value." + ), + .off = offsetof(config_t, hdhomerun_server_tuner_count), + .opts = PO_EXPERT +#if !ENABLE_HDHOMERUN_SERVER + | PO_PHIDDEN +#endif + , + .group = 6, + }, + { + .type = PT_STR, + .id = "hdhomerun_server_model_name", + .name = N_("Tvheadend model name for HDHomeRun Server Emulation"), + .desc = N_("When Tvheadend is acting as an HDHomeRun Server " + "(emulating an HDHomeRun device for downstream " + "media devices to stream Live TV) then " + "we use this as the type of HDHomeRun model number " + "that we send to clients. Some clients may require " + "a specific model number to work. Leave blank " + "for Tvheadend to use a default." + ), + .off = offsetof(config_t, hdhomerun_server_model_name), + .opts = PO_EXPERT +#if !ENABLE_HDHOMERUN_SERVER + | PO_PHIDDEN +#endif + , + .group = 6, + }, + { + .type = PT_BOOL, + .id = "hdhomerun_server_enable", + .name = N_("Enable HDHomeRun Server Emulation"), + .desc = N_("Enable the Tvheadend server to emulate " + "an HDHomeRun server. This allows LiveTV " + "to be used on some media servers." + ), + .off = offsetof(config_t, hdhomerun_server_enable), + .opts = PO_EXPERT +#if !ENABLE_HDHOMERUN_SERVER + | PO_PHIDDEN +#endif +, + .group = 6 + }, { .type = PT_STR, .id = "http_user_agent", diff --git a/src/config.h b/src/config.h index c81c20f75..9d4ee38cd 100644 --- a/src/config.h +++ b/src/config.h @@ -74,6 +74,10 @@ typedef struct config { char *hdhomerun_ip; char *local_ip; int local_port; + char *hdhomerun_server_username; + uint32_t hdhomerun_server_tuner_count; + char *hdhomerun_server_model_name; + int hdhomerun_server_enable; } config_t; extern const idclass_t config_class; diff --git a/src/htsmsg.c b/src/htsmsg.c index 5466cc136..1d3b76c90 100644 --- a/src/htsmsg.c +++ b/src/htsmsg.c @@ -1589,3 +1589,61 @@ htsmsg_remove_string_from_list(htsmsg_t *list, const char *str) } return 0; } + + +// Based on htsbuf_vqprintf, but can't easily share code since we rely +// on stack allocations. +static void +htsmsg_add_str_ap(htsmsg_t *msg, const char *name, const char *fmt, va_list ap0) +{ + // First try to format it on-stack + va_list ap; + int n; + size_t size; + char buf[100], *p, *np; + + va_copy(ap, ap0); + n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + if(n > -1 && n < sizeof(buf)) { + htsmsg_add_str(msg, name, buf); + return; + } + + // Else, do allocations + size = sizeof(buf) * 2; + + p = malloc(size); + while (1) { + /* Try to print in the allocated space. */ + va_copy(ap, ap0); + n = vsnprintf(p, size, fmt, ap); + va_end(ap); + if(n > -1 && n < size) { + htsmsg_add_str(msg, name, p); + // Copy taken by htsmsg_add_str. + free (p); + return; + } + /* Else try again with more space. */ + if (n > -1) /* glibc 2.1 */ + size = n+1; /* precisely what is needed */ + else /* glibc 2.0 */ + size *= 2; /* twice the old size */ + if ((np = realloc (p, size)) == NULL) { + free(p); + abort(); + } else { + p = np; + } + } +} + +void +htsmsg_add_str_printf(htsmsg_t *msg, const char *name, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + htsmsg_add_str_ap(msg, name, fmt, ap); + va_end(ap); +} diff --git a/src/htsmsg.h b/src/htsmsg.h index 1be066e72..bdbfc6321 100644 --- a/src/htsmsg.h +++ b/src/htsmsg.h @@ -213,6 +213,13 @@ void htsmsg_add_str_alloc(htsmsg_t *msg, const char *name, char *str); */ void htsmsg_add_str_exclusive(htsmsg_t *msg, const char *str); +/** + * Add a string using printf-style for the value. + */ +void +htsmsg_add_str_printf(htsmsg_t *msg, const char *name, const char *fmt, ...) + __attribute__((format(printf,3,4)));; + /** * Add/update a string field */ diff --git a/src/htsmsg_json.h b/src/htsmsg_json.h index fae3c4571..ae7cf4cf2 100644 --- a/src/htsmsg_json.h +++ b/src/htsmsg_json.h @@ -29,6 +29,7 @@ htsmsg_t *htsmsg_json_deserialize(const char *src); void htsmsg_json_serialize(htsmsg_t *msg, htsbuf_queue_t *hq, int pretty); +__attribute__((warn_unused_result)) char *htsmsg_json_serialize_to_str(htsmsg_t *msg, int pretty); struct rstr *htsmsg_json_serialize_to_rstr(htsmsg_t *msg, const char *prefix); diff --git a/src/webui/webui.c b/src/webui/webui.c index 3e63aeacc..78e963f96 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -25,6 +25,7 @@ #include "http.h" #include "tcp.h" #include "webui.h" +#include "htsmsg_json.h" #include "dvr/dvr.h" #include "filebundle.h" #include "streaming.h" @@ -1597,6 +1598,304 @@ page_play_auth(http_connection_t *hc, const char *remain, void *opaque) return page_play_(hc, remain, opaque, URLAUTH_CODE); } + + +#if ENABLE_HDHOMERUN_SERVER +/** + * Dummy password verify callback for HDHomeRun. We need this to + * ensure we can get the aa_auth information for the user in to the + * access_t since it is not populated by an access_get_by_username. + * Since the user is configured solely on the server, the "passwd" is + * always valid. + * + * We have the username configured on the server (not the client) + * since HDHomeRun clients usually do not allow entry of credentials. + * + * This is called via the "access_get" call which does IP checking + * (configured via the Users/IP Blocking Records tab in expert + * setting). + */ +static int +hdhomerun_server_verify_callback(void *aux, const char *passwd) +{ + return 1; +} + + +static const char *hdhomerun_get_server_name(void) +{ + return config.server_name ?: "Tvheadend"; +} + +/** + * Our unique device id is calculated from our server's name + * in the general config tab. + */ +static uint32_t hdhomerun_get_deviceid(void) +{ + const char *server_name = hdhomerun_get_server_name(); + const uint32_t deviceid = tvh_crc32((const uint8_t*)server_name, strlen(server_name), 0); + return deviceid; +} + +/// Get the model name, defaulting to a commonly used version. +static const char *hdhomerun_get_model_name(void) +{ + if (config.hdhomerun_server_model_name && !strempty(config.hdhomerun_server_model_name)) + return config.hdhomerun_server_model_name; + else + return "HDTC-2US"; +} + + +/** + * @param fail_log_reason Log this reason if permissions fail. + * @return Permission for the verified user (caller owns the memory) or NULL. + */ +__attribute__((warn_unused_result)) +static access_t *hdhomerun_verify_user_permission(const http_connection_t *hc, + const char *fail_log_reason) +{ + /* Not explicitly enabled? Then all calls fail. */ + if (!config.hdhomerun_server_enable) { + tvhwarn(LS_WEBUI, "hdhomerun server not enabled but received request [%s]", + fail_log_reason?:""); + return NULL; + } + + const char *hdhr_user = config.hdhomerun_server_username ?: ""; + access_t *perm = access_get(hc->hc_peer, hdhr_user, hdhomerun_server_verify_callback, NULL); + + if (access_verify2(perm, ACCESS_STREAMING)) { + /* Failed */ + tvhwarn(LS_WEBUI, "hdhomerun server received request but no streaming permission for user [%s] [%d] [%s]", + hdhr_user ?: "", + perm? perm->aa_rights : 0, + fail_log_reason?:""); + access_destroy(perm); + return NULL; + } else { + return perm; + } +} + + +/** + * Return the discovery information for HDHomeRun to give clients + * details of how to access the lineup. + * + * Our HDHomeRun server implementation uses a server configured + * username to determine access rather than passing it through the + * request. This is because HDHomeRun clients are usually configured + * with only an IP address and port number and do not offer any input + * for credentials. + */ +static int +hdhomerun_server_discover(http_connection_t *hc, const char *remain, void *opaque) +{ + access_t *perm = hdhomerun_verify_user_permission(hc, "discover"); + if (!perm) + return http_noaccess_code(hc); + + char http_ip[128]; + htsbuf_queue_t *hq = &hc->hc_reply; + const char *server_name = hdhomerun_get_server_name(); + const uint32_t deviceid = hdhomerun_get_deviceid(); + + tcp_get_str_from_ip(hc->hc_self, http_ip, sizeof(http_ip)); + + /* The contents below for the discovery message are based on tvhProxy */ + htsmsg_t *msg = htsmsg_create_map(); + htsmsg_add_str(msg, "FriendlyName", server_name); + htsmsg_add_str(msg,"FirmwareVersion", tvheadend_version); + // Currently hardcoded until we encounter a client that has a + // problem. + htsmsg_add_str(msg, "FirmwareName", "hdhomerun_atsc"); + /* We use same value for model name/number to avoid too many user + * configuration options. + */ + htsmsg_add_str(msg, "ModelNumber", hdhomerun_get_model_name()); + htsmsg_add_str(msg, "Manufacturer", "Tvheadend"); + // Random string, but has to be fixed length. + htsmsg_add_str(msg, "DeviceAuth", "3xw5UaJXhVShHEBoy76FuYQi"); + htsmsg_add_str_printf(msg, "BaseURL", "http://%s:%u", http_ip, tvheadend_webui_port); + htsmsg_add_str_printf(msg, "DeviceID", "%08X", deviceid); + htsmsg_add_str_printf(msg, "LineupURL", "http://%s:%u/lineup.json", http_ip, tvheadend_webui_port); + + // If user has not explicitly set a count then we use a default. + // The actual number of tuners is unknown since we allow multiplex + // sharing and some channels may actually be iptv channels so not + // use a tuner at all. + htsmsg_add_u32(msg, "TunerCount", config.hdhomerun_server_tuner_count ?: 6); + + char *json = htsmsg_json_serialize_to_str(msg, 1); + htsbuf_append_str(hq, json); + free(json); + http_output_content(hc, "application/json"); + access_destroy(perm); + return 0; +} + + +/** + * Return the channel lineup for HDHomeRun + */ +static int +hdhomerun_server_lineup(http_connection_t *hc, const char *remain, void *opaque) +{ + access_t *perm = hdhomerun_verify_user_permission(hc, "lineup"); + if (!perm) + return http_noaccess_code(hc); + + htsbuf_queue_t *hq = &hc->hc_reply; + channel_t *ch; + const char *name; + const char *blank; + const char *chnum_str; + const int use_auth = perm && perm->aa_auth && !strempty(perm->aa_auth); + char buf1[128], chnum[32], ubuf[UUID_HEX_SIZE]; + char url[1024]; + char http_ip[128]; + /* We use the UI flags to determine if we should include channel + * numbers/sources in the name. This can help distinguish channels + * when you have multiple different sources of the same channel such + * as satellite and aerial. + */ + const int flags = + (config.chname_num ? CHANNEL_ENAME_NUMBERS : 0) | + (config.chname_src ? CHANNEL_ENAME_SOURCES : 0); + int is_first = 1; + + tcp_get_str_from_ip(hc->hc_self, http_ip, sizeof(http_ip)); + blank = tvh_gettext_lang(perm->aa_lang_ui, channel_blank_name); + htsbuf_append_str(hq, "["); + tvh_mutex_lock(&global_lock); + CHANNEL_FOREACH(ch) { + if (!channel_access(ch, perm, 0) || !ch->ch_enabled) + continue; + if (!is_first) + htsbuf_append_str(hq, ", \n"); + name = channel_get_ename(ch, buf1, sizeof(buf1), blank, flags); + htsbuf_append_str(hq, "{ \"GuideName\" : "); + htsbuf_append_and_escape_jsonstr(hq, name); + htsbuf_append_str(hq, ", \"GuideNumber\" : "); + /* channel_get_number_as_str returns NULL if no channel number! */ + chnum_str = channel_get_number_as_str(ch, chnum, sizeof(chnum)); + htsbuf_append_and_escape_jsonstr(hq, chnum_str ? chnum_str : "0"); + htsbuf_append_str(hq, ", \"URL\" : "); + sprintf(url, "http://%s:%u/stream/channel/%s?profile=pass%s%s", + http_ip, + tvheadend_webui_port, + channel_get_uuid(ch, ubuf), + use_auth? "&auth=" : "", + use_auth ? perm->aa_auth : ""); + htsbuf_append_and_escape_jsonstr(hq, url); + htsbuf_append_str(hq, "}"); + is_first = 0; + } + tvh_mutex_unlock(&global_lock); + htsbuf_append_str(hq, "]"); + http_output_content(hc, "application/json"); + access_destroy(perm); + return 0; +} + + +static int +hdhomerun_server_lineup_status(http_connection_t *hc, const char *remain, void *opaque) +{ + access_t *perm = hdhomerun_verify_user_permission(hc, "lineup_status"); + if (!perm) + return http_noaccess_code(hc); + + htsbuf_queue_t *hq = &hc->hc_reply; + /* The contents below for the discovery message are based on the forum. */ + htsbuf_append_str(hq, "{\"ScanInProgress\":0,\"ScanPossible\":0,\"Source\":\"Antenna\",\"SourceList\":[\"Antenna\"]}"); + http_output_content(hc, "application/json"); + access_destroy(perm); + return 0; +} + + + +/** Some media players ignore the "scan not possible" and do a post to + * this function with "?scan=start". + * + * We currently ignore this request and just return success. + * This is because Tvheadend has separate scanning and mapping stages. + */ +static int +hdhomerun_server_lineup_post(http_connection_t *hc, const char *remain, void *opaque) +{ + access_t *perm = hdhomerun_verify_user_permission(hc, "lineup_status"); + if (!perm) + return http_noaccess_code(hc); + + // We can't send empty contents since the caller thinks empty (size + // 0) is "unknown length" (for streaming data). So, we'll return an + // empty json document. + htsbuf_append_str(&hc->hc_reply, "{}"); + http_output_content(hc, "application/json"); + access_destroy(perm); + return 0; +} + +/** + * Needed for some clients. This contains much the same as discover, + * but in xml format. + */ +static int +hdhomerun_server_device_xml(http_connection_t *hc, const char *remain, void *opaque) +{ + access_t *perm = hdhomerun_verify_user_permission(hc, "device.xml"); + if (!perm) + return http_noaccess_code(hc); + + const char *server_name = hdhomerun_get_server_name(); + const char *model_name = hdhomerun_get_model_name(); + /* Need to escape strings in xml */ + char server_name_escaped[128]; + char model_name_escaped[128]; + char http_ip[128]; + htsbuf_queue_t *hq = &hc->hc_reply; + const uint32_t deviceid = hdhomerun_get_deviceid(); + + html_escape(server_name_escaped, server_name, sizeof(server_name_escaped)); + html_escape(model_name_escaped, model_name, sizeof(model_name_escaped)); + + tcp_get_str_from_ip(hc->hc_self, http_ip, sizeof(http_ip)); + htsbuf_qprintf(hq, "" + "" + "1" + "0" + "" + "http://%s:%u" + "" + "urn:schemas-upnp-org:device:MediaServer:1" + "%s" + "Tvheadend" + "%s" + "%s" + "" + /* Version 5 UUID (random) with top part as server id*/ + "uuid:%8.8x-745e-5d9a-8903-4a02327a7e09" + "" + "", + http_ip, tvheadend_webui_port, + server_name_escaped, + // We'll use the same for model name and number to + // avoid too much user configuration. Some clients + // may use the model name to infer characteristics. + model_name_escaped, + model_name_escaped, + deviceid); + + http_output_content(hc, "application/xml"); + access_destroy(perm); + return 0; +} +#endif /* ENABLE_HDHOMERUN_SERVER */ + /** * */ @@ -2134,6 +2433,16 @@ webui_init(int xspf) http_path_add("/satip_server", NULL, satip_server_http_page, ACCESS_ANONYMOUS); #endif +#if ENABLE_HDHOMERUN_SERVER + /* These names are specified in https://info.hdhomerun.com/info/http_api */ + http_path_add("/discover.json", NULL, hdhomerun_server_discover, ACCESS_ANONYMOUS); + http_path_add("/lineup.json", NULL, hdhomerun_server_lineup, ACCESS_ANONYMOUS); + /* These names are not specified in the documents but are required to make Plex work. */ + http_path_add("/lineup_status.json", NULL, hdhomerun_server_lineup_status, ACCESS_ANONYMOUS); + http_path_add("/lineup.post", NULL, hdhomerun_server_lineup_post, ACCESS_ANONYMOUS); + http_path_add("/device.xml", NULL, hdhomerun_server_device_xml, ACCESS_ANONYMOUS); +#endif + http_path_add_modify("/play", NULL, page_play, ACCESS_ANONYMOUS, page_play_path_modify5); http_path_add_modify("/play/ticket", NULL, page_play_ticket, ACCESS_ANONYMOUS, page_play_path_modify12); http_path_add_modify("/play/auth", NULL, page_play_auth, ACCESS_ANONYMOUS, page_play_path_modify10); base-commit: 8a2942a361e95ccdbd30c1edc7627df3862cdbbe -- 2.25.1