From 444712de99f020ad5194e97e9d682ddc8a731b47 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] 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 there does not appear to be limited documentation on the contents of the message that the discovery process sends. 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. 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. --- configure | 1 + .../config_hdhomerun_server_username.md | 23 ++ src/config.c | 40 +++ src/config.h | 2 + src/htsbuf.c | 9 + src/htsbuf.h | 2 + src/webui/webui.c | 254 ++++++++++++++++++ 7 files changed, 331 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..e1e9cf81e --- /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..2f56d4a6c 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,45 @@ 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. " + "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_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..c7391e8d1 100644 --- a/src/config.h +++ b/src/config.h @@ -74,6 +74,8 @@ typedef struct config { char *hdhomerun_ip; char *local_ip; int local_port; + char *hdhomerun_server_username; + int hdhomerun_server_enable; } config_t; extern const idclass_t config_class; diff --git a/src/htsbuf.c b/src/htsbuf.c index 50f22a500..935f37ce6 100644 --- a/src/htsbuf.c +++ b/src/htsbuf.c @@ -539,6 +539,15 @@ htsbuf_append_and_escape_jsonstr(htsbuf_queue_t *hq, const char *str) +void htsbuf_append_and_escape_jsonstr_nv(htsbuf_queue_t *hq, const char *name, const char *value) +{ + htsbuf_append_and_escape_jsonstr(hq, name); + htsbuf_append_str(hq, ": "); + htsbuf_append_and_escape_jsonstr(hq, value); +} + + + /** * */ diff --git a/src/htsbuf.h b/src/htsbuf.h index 093299400..1b95e018c 100644 --- a/src/htsbuf.h +++ b/src/htsbuf.h @@ -82,6 +82,8 @@ void htsbuf_append_and_escape_url(htsbuf_queue_t *hq, const char *s); void htsbuf_append_and_escape_rfc8187(htsbuf_queue_t *hq, const char *s); void htsbuf_append_and_escape_jsonstr(htsbuf_queue_t *hq, const char *s); +/** Append a name and value JSON string */ +void htsbuf_append_and_escape_jsonstr_nv(htsbuf_queue_t *hq, const char *name, const char *value); void htsbuf_dump_raw_stderr(htsbuf_queue_t *hq); diff --git a/src/webui/webui.c b/src/webui/webui.c index 3e63aeacc..af9e279f1 100644 --- a/src/webui/webui.c +++ b/src/webui/webui.c @@ -1597,6 +1597,251 @@ 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; +} + + +/** + * @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 */ + htsbuf_append_str(hq, "{ "); + htsbuf_append_and_escape_jsonstr_nv(hq, "FriendlyName", server_name); + htsbuf_qprintf(hq, ", " \ + "\"BaseURL\" : \"http://%s:%u\", " \ + "\"DeviceAuth\": \"3xw5UaJXhVShHEBoy76FuYQi\", " \ + "\"DeviceID\": \"%08X\", " \ + "\"FirmwareName\": \"hdhomerun_atsc\", " \ + "\"FirmwareVersion\": \"20200101\", " \ + "\"LineupURL\": \"http://%s:%u/lineup.json\", " \ + "\"Manufacturer\": \"Silicondust\", " \ + "\"ModelNumber\": \"HDTC-2US\", " \ + "\"TunerCount\": 6 " \ + "}", + http_ip, tvheadend_webui_port, + deviceid, + http_ip, tvheadend_webui_port); + 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; +} + + + +/** + * 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(); + char http_ip[128]; + htsbuf_queue_t *hq = &hc->hc_reply; + const uint32_t deviceid = hdhomerun_get_deviceid(); + + 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" + "Silicondust" + "HDTC-2US" + "HDTC-2US" + "" + /* Version 5 UUID (random) with top part as server id*/ + "uuid:%8.8x-745e-5d9a-8903-4a02327a7e09" + "" + "", + http_ip, tvheadend_webui_port, + server_name, + deviceid); + + http_output_content(hc, "application/xml"); + access_destroy(perm); + return 0; +} +#endif /* ENABLE_HDHOMERUN_SERVER */ + /** * */ @@ -2134,6 +2379,15 @@ 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("/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); -- 2.25.1