diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index 4f40058..e65d7a7 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -20,6 +20,12 @@ rtp2httpd [options] - `-m, --maxclients ` - Maximum concurrent clients (default: 5) - `-w, --workers ` - Number of worker processes (default: 1) +`--listen` can be specified multiple times to listen on multiple addresses or ports: + +```bash +rtp2httpd --listen 5140 --listen 192.168.1.1:8081 --listen '[::1]:5140' +``` + #### Upstream Network Interface Configuration - `-i, --upstream-interface ` - Default upstream interface (applies to all traffic types, lowest priority) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 6291188..7343b91 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,6 +20,12 @@ rtp2httpd [选项] - `-m, --maxclients <数量>` - 最大并发客户端数 (默认: 5) - `-w, --workers <数量>` - 工作进程数 (默认: 1) +`--listen` 可以重复指定,用于同时监听多个地址或端口: + +```bash +rtp2httpd --listen 5140 --listen 192.168.1.1:8081 --listen '[::1]:5140' +``` + #### 上游网络接口配置 - `-i, --upstream-interface <接口>` - 默认上游接口(作用于所有流量类型,优先级最低) diff --git a/e2e/test_config.py b/e2e/test_config.py index 901779c..2572ac5 100644 --- a/e2e/test_config.py +++ b/e2e/test_config.py @@ -386,6 +386,21 @@ def test_listen_port(self, r2h_binary): finally: r2h.stop() + def test_multiple_listen_ports(self, r2h_binary): + """Repeated -l flags should expose the HTTP server on every port.""" + port1 = find_free_port() + port2 = find_free_port() + r2h = R2HProcess(r2h_binary, port1, extra_args=["-v", "4", "-l", str(port2)]) + try: + r2h.start() + + for port in (port1, port2): + assert wait_for_port(port) + status, _, _ = http_get("127.0.0.1", port, "/status") + assert status == 200 + finally: + r2h.stop() + # --------------------------------------------------------------------------- # Max clients (-m) diff --git a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js index 64f6d4e..9da6e31 100644 --- a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js +++ b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js @@ -6,6 +6,115 @@ "require uci"; return view.extend({ + normalizeListenValues: function (value) { + var input = Array.isArray(value) ? value : value ? [value] : []; + var values = []; + + for (var i = 0; i < input.length; i++) { + var listen = String(input[i]).trim(); + if (listen) { + values.push(listen); + } + } + + return values; + }, + + getListenValues: function (section_id) { + return this.normalizeListenValues( + uci.get("rtp2httpd", section_id, "listen") + ); + }, + + parseListenValue: function (value) { + var listen = String(value || "").trim(); + var host = null; + var port = null; + var match; + var pos; + + if (!listen) { + return null; + } + + if (/^\d+$/.test(listen)) { + port = listen; + } else if (listen.charAt(0) === "[") { + match = listen.match(/^\[([^\]]+)\]:(\d+)$/); + if (!match) { + return null; + } + host = "[" + match[1] + "]"; + port = match[2]; + } else { + pos = listen.lastIndexOf(":"); + if (pos <= 0 || pos !== listen.indexOf(":")) { + return null; + } + host = listen.substring(0, pos); + port = listen.substring(pos + 1); + } + + if (!/^\d+$/.test(port)) { + return null; + } + + var portNumber = Number(port); + if (portNumber < 1 || portNumber > 65535) { + return null; + } + + if (host !== null) { + if ( + host === "*" || + host.indexOf("/") >= 0 || + /\s/.test(host) + ) { + return null; + } + } + + return { + host: host, + port: port, + }; + }, + + validateListenValue: function (value) { + var listen = String(value || "").trim(); + + if (!listen) { + return true; + } + + if (/^\*:/.test(listen)) { + return _( + "Use a bare port such as 5140 to listen on all addresses; *:5140 is not supported here." + ); + } + + if (!this.parseListenValue(listen)) { + return _( + "Use port, address:port, hostname:port, or [IPv6]:port, for example 5140 or 192.168.1.1:8081." + ); + } + + return true; + }, + + getPrimaryListenTarget: function (section_id) { + var values = this.getListenValues(section_id); + var target = values.length > 0 ? this.parseListenValue(values[0]) : null; + var port; + + if (target) { + return target; + } + + port = uci.get("rtp2httpd", section_id, "port") || "5140"; + return this.parseListenValue(port) || { host: null, port: "5140" }; + }, + // Helper function to open a page (status or player) openPage: function (section_id, pageType) { var pathConfigKey = @@ -24,6 +133,7 @@ return view.extend({ var token = null; var pagePath = defaultPath; var hostname = null; + var listenTarget = null; var use_config_file = uci.get("rtp2httpd", section_id, "use_config_file"); if (use_config_file === "1") { @@ -59,8 +169,9 @@ return view.extend({ pagePath = pagePathMatch[1]; } } else { - // Get port, token, hostname and page path from UCI config - port = uci.get("rtp2httpd", section_id, "port") || "5140"; + // Get listen address, token, hostname and page path from UCI config + listenTarget = self.getPrimaryListenTarget(section_id); + port = listenTarget.port; token = uci.get("rtp2httpd", section_id, "r2h_token"); hostname = uci.get("rtp2httpd", section_id, "hostname"); pagePath = uci.get("rtp2httpd", section_id, uciPathKey) || defaultPath; @@ -72,7 +183,10 @@ return view.extend({ } // Use configured hostname or fallback to window.location.hostname - var targetHostname = hostname || window.location.hostname; + var targetHostname = + hostname || + (listenTarget && listenTarget.host) || + window.location.hostname; // If hostname doesn't have protocol, prepend http:// for URL parsing var hasProtocol = /^https?:\/\//i.test(targetHostname); @@ -100,6 +214,10 @@ return view.extend({ var finalHost = url.hostname; var finalPort = ""; + if (finalHost.indexOf(":") >= 0 && finalHost.charAt(0) !== "[") { + finalHost = "[" + finalHost + "]"; + } + if (!hasProtocol) { // No protocol in original hostname: use configured port if URL port is empty if (!url.port) { @@ -234,10 +352,58 @@ return view.extend({ }); }; - o = s.taboption("basic", form.Value, "port", _("Port")); - o.datatype = "port"; + o = s.taboption( + "basic", + form.DynamicList, + "listen", + _("Listen Addresses"), + _( + "HTTP listen addresses. Use a bare port for all addresses (e.g., 5140), address:port for IPv4/hostnames, or [IPv6]:port." + ) + ); o.placeholder = "5140"; + o.rmempty = true; o.depends("use_config_file", "0"); + o.cfgvalue = function (section_id) { + var values = self.getListenValues(section_id); + var port; + + if (values.length > 0) { + return values; + } + + port = uci.get("rtp2httpd", section_id, "port"); + return port ? [port] : []; + }; + o.formvalue = function (section_id) { + var elem = this.getUIElement(section_id); + + return self.normalizeListenValues(elem ? elem.getValue() : null); + }; + o.write = function (section_id, value) { + var values = self.normalizeListenValues(value); + var config = this.uciconfig || this.section.uciconfig || this.map.config; + var sid = this.ucisection || section_id; + var option = this.ucioption || this.option; + + if (values.length > 0) { + this.map.data.set(config, sid, option, values); + } else { + this.map.data.unset(config, sid, option); + } + + this.map.data.unset(config, sid, "port"); + }; + o.remove = function (section_id) { + var config = this.uciconfig || this.section.uciconfig || this.map.config; + var sid = this.ucisection || section_id; + + this.map.data.unset(config, sid, this.ucioption || this.option); + this.map.data.unset(config, sid, "port"); + }; + o.validate = function (section_id, value) { + return self.validateListenValue(value); + }; o = s.taboption("basic", form.ListValue, "verbose", _("Logging level")); o.value("0", "Fatal"); diff --git a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot index e8b346f..fc2604f 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot +++ b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot @@ -144,7 +144,17 @@ msgid "" "40000-40100). Leave empty to use random ports." msgstr "" -#: htdocs/luci-static/resources/view/rtp2httpd.js:242 +#: htdocs/luci-static/resources/view/rtp2httpd.js:361 +msgid "" +"HTTP listen addresses. Use a bare port for all addresses (e.g., 5140), " +"address:port for IPv4/hostnames, or [IPv6]:port." +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:359 +msgid "Listen Addresses" +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:300 msgid "Logging level" msgstr "" @@ -224,10 +234,6 @@ msgstr "" msgid "Please configure External M3U URL first" msgstr "" -#: htdocs/luci-static/resources/view/rtp2httpd.js:237 -msgid "Port" -msgstr "" - #: htdocs/luci-static/resources/view/rtp2httpd.js:540 msgid "R2H Token" msgstr "" @@ -251,6 +257,18 @@ msgid "" "disable CORS." msgstr "" +#: htdocs/luci-static/resources/view/rtp2httpd.js:92 +msgid "" +"Use a bare port such as 5140 to listen on all addresses; *:5140 is not " +"supported here." +msgstr "" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:98 +msgid "" +"Use port, address:port, hostname:port, or [IPv6]:port, for example 5140 or " +"192.168.1.1:8081." +msgstr "" + #: htdocs/luci-static/resources/view/rtp2httpd.js:507 msgid "Status Dashboard" msgstr "" diff --git a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po index 82e2545..facc284 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po +++ b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po @@ -164,7 +164,19 @@ msgstr "" "FCC 客户端套接字使用的本地 UDP 端口范围(格式:起始端口-结束端口,例如:" "40000-40100)。留空则使用随机端口。" -#: htdocs/luci-static/resources/view/rtp2httpd.js:242 +#: htdocs/luci-static/resources/view/rtp2httpd.js:361 +msgid "" +"HTTP listen addresses. Use a bare port for all addresses (e.g., 5140), " +"address:port for IPv4/hostnames, or [IPv6]:port." +msgstr "" +"HTTP 监听地址。使用裸端口表示监听所有地址(例如:5140),IPv4/主机名使用 " +"address:port,IPv6 使用 [IPv6]:port。" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:359 +msgid "Listen Addresses" +msgstr "监听地址" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:300 msgid "Logging level" msgstr "日志级别" @@ -252,10 +264,6 @@ msgstr "播放器页面路径" msgid "Please configure External M3U URL first" msgstr "请先配置外部 M3U URL" -#: htdocs/luci-static/resources/view/rtp2httpd.js:237 -msgid "Port" -msgstr "端口" - #: htdocs/luci-static/resources/view/rtp2httpd.js:540 msgid "R2H Token" msgstr "HTTP 请求认证令牌" @@ -281,6 +289,20 @@ msgstr "" "设置 Access-Control-Allow-Origin 响应头以启用 CORS 跨域访问。设为 * 允许所有" "来源,或指定域名(如 https://example.com)。留空则禁用 CORS。" +#: htdocs/luci-static/resources/view/rtp2httpd.js:92 +msgid "" +"Use a bare port such as 5140 to listen on all addresses; *:5140 is not " +"supported here." +msgstr "使用 5140 这样的裸端口监听所有地址;这里不支持 *:5140。" + +#: htdocs/luci-static/resources/view/rtp2httpd.js:98 +msgid "" +"Use port, address:port, hostname:port, or [IPv6]:port, for example 5140 or " +"192.168.1.1:8081." +msgstr "" +"请使用端口、address:port、hostname:port 或 [IPv6]:port 格式,例如 5140 或 " +"192.168.1.1:8081。" + #: htdocs/luci-static/resources/view/rtp2httpd.js:507 msgid "Status Dashboard" msgstr "状态面板" diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.conf b/openwrt-support/rtp2httpd/files/rtp2httpd.conf index 5d3875e..c3b8537 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.conf +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.conf @@ -3,6 +3,13 @@ config rtp2httpd option respawn '1' # option use_config_file '0' # option verbose '1' + + # HTTP listen addresses. Use a bare port to listen on all addresses. + # Multiple listen addresses are supported. + # list listen '5140' + # list listen '192.168.1.1:8081' + # list listen '[::1]:5140' + # Legacy single-port option, kept for backward compatibility: # option port '5140' # Upstream Interface Configuration diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.init b/openwrt-support/rtp2httpd/files/rtp2httpd.init index bfb60c2..72bbe6f 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.init +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.init @@ -14,6 +14,27 @@ append_arg() { [ -n "$val" -o -n "$def" ] && procd_append_param command $opt "${val:-$def}" } +append_listen_arg() { + local val="$1" + + [ -n "$val" ] || return + procd_append_param command "--listen" "$val" + rtp2httpd_listen_count=$((rtp2httpd_listen_count + 1)) +} + +append_listen_args() { + local cfg="$1" + + rtp2httpd_listen_count=0 + config_list_foreach "$cfg" listen append_listen_arg + + # Backward compatibility for existing installations using option port. + if [ "$rtp2httpd_listen_count" -eq 0 ]; then + append_arg "$cfg" port "--listen" || true + fi + return 0 +} + start_instance() { local cfg="$1" local aux @@ -34,7 +55,7 @@ start_instance() { procd_append_param command "--noconfig" append_arg "$cfg" verbose "--verbose" - append_arg "$cfg" port "--listen" + append_listen_args "$cfg" # Interface configuration - check advanced settings mode config_get_bool advanced_interface_settings "$cfg" 'advanced_interface_settings' '0'