diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d5dab4c0..d631c10b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog + +# v1.1.7(2024-01-xx) +* Renamed api-proxy.yml server info field `ip` to `host` +* In api-proxy.yml assign server config to user + # v1.1.6(2024-01-17) -* watch filter are now regular expressions +* Watch filter are now regular expressions * Fixed watch file not created problem * UI responds immediately to update request diff --git a/README.md b/README.md index 20d577814..a4a59b493 100644 --- a/README.md +++ b/README.md @@ -658,10 +658,17 @@ you need to create a `api-proxy.yml` configuration. You can specify the path for the file with the `-a` cli argument. +The configuration contains the server info for xtream accounts and user definitions. +You can define multiple server with unique names, one should be named `default`. + +Iptv player can act differently and use the direct-source attribute or can compose the url based on the server info. +You should set `xtream_skip_live_direct_source` and `xtream_skip_video_direct_source` to `true` to avoid this problem. + `username` and `password`are mandatory for credentials. `username` is unique. The `token` is _optional_. If defined it should be unique. The `token`can be used instead of username+password `proxy` is _optional_. If defined it can be `reverse` or `redirect`. Default is `redirect`. +`server` is _optional_. It should match one server definition, if not given the server with the name `default` is used or the first one. To access the api for: - `xtream` use url like `http://192.169.1.2/player_api.php?username={}&password={}` @@ -684,19 +691,30 @@ When you define credentials for a `target`, ensure that this target has `output` format `xtream`or `m3u`. +If you use `https` you need a ssl terminator. `m3u-filter` does not support https traffic. + ```yaml server: - protocol: http - ip: 192.168.0.3 - http_port: 80 - https_port: 443 - rtmp_port: 0 - timezone: Europe/Paris - message: Welcome to m3u-filter + - name: default + protocol: http + host: 192.168.0.3 + http_port: 80 + https_port: 443 + rtmp_port: 0 + timezone: Europe/Paris + message: Welcome to m3u-filter + - name: external + protocol: http + host: my_external_domain.com + http_port: 80 + https_port: 443 + rtmp_port: 0 + timezone: Europe/Paris + message: Welcome to m3u-filter user: - target: pl1 credentials: - - {username: x3452, password: ztrhgrGZ, token: 4342sd, proxy: reverse} + - {username: x3452, password: ztrhgrGZ, token: 4342sd, proxy: reverse, server: external} - {username: x3451, password: secret, token: abcde, proxy: redirect} ``` diff --git a/api-proxy.yml b/api-proxy.yml index 599333ba3..76f784828 100644 --- a/api-proxy.yml +++ b/api-proxy.yml @@ -1,12 +1,22 @@ server: - protocol: http - ip: 192.168.9.3 - http_port: 80 - https_port: - rtmp_port: - timezone: Europe/Paris - message: Welcome to m3u-filter + - name: default + protocol: http + host: 192.168.9.3 + http_port: 80 + https_port: + rtmp_port: + timezone: Europe/Paris + message: Welcome to m3u-filter + - name: extern + protocol: https + host: m3u-filter.mydns.com + http_port: 8900 + https_port: + rtmp_port: + timezone: Europe/Paris + message: Welcome to m3u-filter user: - target: pl1 credentials: - - {username: x3452, password: ztrhgrGZrt83hjerter, token: 4243234wer234er234, proxy: redirect} \ No newline at end of file + - {username: x3452, password: ztrhgrGZrt83hjerter, token: 4243234wer234er234, proxy: redirect, server: default} + - {username: x3453, password: kljlkjorn87jfkhksdh, token: 43dfgerrt345345ss4, proxy: reverse, server: extern} \ No newline at end of file diff --git a/frontend/src/api/config-api-service.ts b/frontend/src/api/config-api-service.ts index c72d59dec..585315843 100644 --- a/frontend/src/api/config-api-service.ts +++ b/frontend/src/api/config-api-service.ts @@ -14,7 +14,7 @@ export default interface ConfigApiService extends ApiService { saveTargetUser(targetUser: TargetUser[]): Observable; - saveApiProxyConfig(serverInfo: ApiProxyServerInfo): Observable; + saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]): Observable; } export class DefaultConfigApiService extends DefaultApiService implements ConfigApiService { @@ -30,7 +30,7 @@ export class DefaultConfigApiService extends DefaultApiService implements Config return this.post(SERVER_CONFIG_TARGET_USER_API_PATH, targetUser); } - saveApiProxyConfig(serverInfo: ApiProxyServerInfo): Observable { + saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]): Observable { return this.post(SERVER_CONFIG_SERVER_INFO_API_PATH, serverInfo); } diff --git a/frontend/src/component/api-proxy-view/api-proxy-view.scss b/frontend/src/component/api-proxy-view/api-proxy-view.scss index 895a287ae..bd56953ff 100644 --- a/frontend/src/component/api-proxy-view/api-proxy-view.scss +++ b/frontend/src/component/api-proxy-view/api-proxy-view.scss @@ -1,12 +1,26 @@ @use '../../scss/common'; @use '../../scss/preferences'; + .api-proxy { @include preferences.prefsRoot(); @include preferences.prefsToolbar(); @include preferences.prefsForm(); + &__content { + display: flex; + flex-flow: column; + gap: 8px; + overflow: auto; + max-height: 480px; + &-area { + display: flex; + flex-flow: column; + gap: 8px; + } + } + &__content-help{ display: flex; flex-flow: row; diff --git a/frontend/src/component/api-proxy-view/api-proxy-view.tsx b/frontend/src/component/api-proxy-view/api-proxy-view.tsx index 80ed1c068..77f675902 100644 --- a/frontend/src/component/api-proxy-view/api-proxy-view.tsx +++ b/frontend/src/component/api-proxy-view/api-proxy-view.tsx @@ -11,9 +11,10 @@ const isNumber = (value: string): boolean => { } const SERVER_INFO_FIELDS = [ + {name: 'name', label: 'Name', fieldType: FormFieldType.READONLY}, {name: 'protocol', label: 'Protocol', fieldType: FormFieldType.SINGLE_SELECT, options:[{value: 'http', label:'http'}, {value: 'https', label:'https'}]}, - {name: 'ip', label: 'IP', fieldType: FormFieldType.TEXT}, + {name: 'host', label: 'Host', fieldType: FormFieldType.TEXT}, {name: 'http_port', label: 'HTTP port', fieldType: FormFieldType.NUMBER, validator: isNumber}, {name: 'https_port', label: 'HTTPS port', fieldType: FormFieldType.NUMBER,validator: isNumber}, {name: 'rtmp_port', label: 'RTMP port', fieldType: FormFieldType.NUMBER,validator: isNumber}, @@ -29,7 +30,7 @@ export default function ApiProxyView(props: ApiProxyViewProps) { const {config} = props; const services = useServices(); const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar(); - const serverInfo = useMemo(() => config?.api_proxy?.server, [config]); + const serverInfo = useMemo(() => config?.api_proxy?.server, [config]); const handleSave = useCallback(() => { if (serverInfo) { @@ -46,7 +47,12 @@ export default function ApiProxyView(props: ApiProxyViewProps) {
- +
+ {serverInfo?.map(server =>
+ +
) + } +
{getIconByName('Warn')} diff --git a/frontend/src/component/form-view/from-view.tsx b/frontend/src/component/form-view/from-view.tsx index deccf0281..0f0cab7c2 100644 --- a/frontend/src/component/form-view/from-view.tsx +++ b/frontend/src/component/form-view/from-view.tsx @@ -10,6 +10,7 @@ import TagInput from "../tag-input/tag-input"; // } export enum FormFieldType { + READONLY= 'readonly', TEXT = 'text', NUMBER = 'number', MULTI_SELECT = 'multi_select', @@ -56,6 +57,8 @@ export default function FormView(props: FormViewProps) { const getFieldInput = useCallback((field: FormField) => { switch (field.fieldType) { + case FormFieldType.READONLY: + return {data?.[field.name]}; case FormFieldType.CHECK: return case FormFieldType.MULTI_SELECT: diff --git a/frontend/src/component/user-view/user-view.scss b/frontend/src/component/user-view/user-view.scss index 8c7abe685..6fe2e9c8b 100644 --- a/frontend/src/component/user-view/user-view.scss +++ b/frontend/src/component/user-view/user-view.scss @@ -10,7 +10,7 @@ @include preferences.prefsToolbar(); min-height: 400px; - max-height: 400px; + max-height: 480px; &__content { display: flex; @@ -81,6 +81,7 @@ &-table { margin: 8px 4px; display: table; + table-layout: auto; width: auto; border-collapse: collapse; } @@ -104,6 +105,9 @@ vertical-align: bottom; input { border: none !important; + @media (min-width: 981px) { + width: 100%; + } } &-toolbar { diff --git a/frontend/src/component/user-view/user-view.tsx b/frontend/src/component/user-view/user-view.tsx index 23b2eb8be..e3dde550a 100644 --- a/frontend/src/component/user-view/user-view.tsx +++ b/frontend/src/component/user-view/user-view.tsx @@ -25,9 +25,12 @@ export default function UserView(props: UserViewProps) { const [targets, setTargets] = useState([]); const [activeTarget, setActiveTarget] = useState(undefined); const [tabs, setTabs] = useState([]); + const [serverOptions, setServerOptions] = useState<{value: string, label: string}[]>([]); useEffect(() => { if (config) { + const serverOptions = config?.api_proxy?.server?.map(serverInfo => ({ value: serverInfo.name, label: serverInfo.name })); + setServerOptions(serverOptions || []); const target_names = ConfigUtils.getTargetNames(config); const missing = config?.api_proxy?.user.filter(target => !target_names.includes(target.target)); const result: TargetUser[] = target_names?.map(name => ({ @@ -106,12 +109,13 @@ export default function UserView(props: UserViewProps) { }, [targets]); const handleChange = useCallback((fieldWithTargetAndIndex: string, value: any) => { - const sep_idx = fieldWithTargetAndIndex.lastIndexOf('-'); - const target_name = fieldWithTargetAndIndex.substring(0, sep_idx); + const parts = fieldWithTargetAndIndex.split('@'); + const target_name = parts[0]; const target = targets.find(target => target.target === target_name); if (target) { - const idx = parseInt(fieldWithTargetAndIndex.substring(sep_idx+1)); - target.credentials[idx].proxy = value; + const idx = parseInt(parts[1]); + const credentials: any = target.credentials[idx]; + credentials[parts[2]] = value; } }, [targets]); @@ -176,6 +180,8 @@ export default function UserView(props: UserViewProps) {
+
+
@@ -193,13 +199,18 @@ export default function UserView(props: UserViewProps) { data-field={field} onChange={handleValueChange}> )} +
+
+ +
-
-
{getIconByName('PersonRemove')} diff --git a/frontend/src/config.ts b/frontend/src/config.ts index dea3a7092..c42b68c12 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -5,7 +5,7 @@ const dev = { version: process.env.REACT_APP_VERSION, }, api: { - serverUrl: 'http://localhost:8901/api/v1/', + serverUrl: 'http://10.41.41.41:8901/api/v1/', } }; diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts index 6c5f58aa9..72cac010a 100644 --- a/frontend/src/model/server-config.ts +++ b/frontend/src/model/server-config.ts @@ -118,8 +118,9 @@ export interface TargetUser { } export interface ApiProxyServerInfo { + name: string; protocol: string; - ip: string; + host: string; http_port: string; https_port: string; rtmp_port: string; @@ -128,7 +129,7 @@ export interface ApiProxyServerInfo { } export interface ApiProxyConfig { - server: ApiProxyServerInfo; + server: ApiProxyServerInfo[]; user: TargetUser[]; } diff --git a/frontend/src/service/config-service.ts b/frontend/src/service/config-service.ts index 8364c4c14..fbca44203 100644 --- a/frontend/src/service/config-service.ts +++ b/frontend/src/service/config-service.ts @@ -18,7 +18,7 @@ export default class ConfigService { return this.configApiService.saveTargetUser(targetUser); } - saveApiProxyConfig(serverInfo: ApiProxyServerInfo) { + saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]) { return this.configApiService.saveApiProxyConfig(serverInfo); } } diff --git a/src/api/v1_api.rs b/src/api/v1_api.rs index 694976a69..bfa3ce452 100644 --- a/src/api/v1_api.rs +++ b/src/api/v1_api.rs @@ -67,21 +67,23 @@ pub(crate) async fn save_config_main( pub(crate) async fn save_config_api_proxy_config( - mut req: web::Json, + req: web::Json>, mut _app_state: web::Data, ) -> HttpResponse { - if req.0.is_valid() { - if let Some(api_proxy) = _app_state.config._api_proxy.write().unwrap().as_mut() { - api_proxy.server = req.0; - let backup_dir = _app_state.config.backup_dir.as_ref().unwrap().as_str(); - if let Some(err) = _save_config_api_proxy(backup_dir, api_proxy) { - return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); - } + let mut req_api_proxy = req.0; + for server_info in &mut req_api_proxy { + if ! server_info.is_valid() { + return HttpResponse::BadRequest().json(json!({"error": "Invalid content"})); } - HttpResponse::Ok().finish() - } else { - HttpResponse::BadRequest().json(json!({"error": "Invalid content"})) } + if let Some(api_proxy) = _app_state.config._api_proxy.write().unwrap().as_mut() { + api_proxy.server = req_api_proxy; + let backup_dir = _app_state.config.backup_dir.as_ref().unwrap().as_str(); + if let Some(err) = _save_config_api_proxy(backup_dir, api_proxy) { + return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); + } + } + HttpResponse::Ok().finish() } pub(crate) async fn playlist_update( diff --git a/src/api/xtream_player_api.rs b/src/api/xtream_player_api.rs index 51d6ce2fe..1985cc8c5 100644 --- a/src/api/xtream_player_api.rs +++ b/src/api/xtream_player_api.rs @@ -57,7 +57,16 @@ fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &str, action_p fn get_user_info(user: &UserCredentials, cfg: &Config) -> XtreamAuthorizationResponse { - let server = cfg._api_proxy.read().unwrap().as_ref().unwrap().server.clone(); + let server_info_list = cfg._api_proxy.read().unwrap().as_ref().unwrap().server.clone(); + let server_info_name = match &user.server { + Some(server_name) => server_name.as_str(), + None => "default" + }; + let server_info = match server_info_list.iter().find(|c| c.name.eq(server_info_name)) { + Some(info) => info, + None => server_info_list.first().unwrap(), + }; + let now = Local::now(); XtreamAuthorizationResponse { user_info: XtreamUserInfo { @@ -68,18 +77,18 @@ fn get_user_info(user: &UserCredentials, cfg: &Config) -> XtreamAuthorizationRes exp_date: (now + Duration::days(365)).timestamp(),// fake is_trial: "0".to_string(), max_connections: "1".to_string(), - message: server.message.to_string(), + message: server_info.message.to_string(), password: user.password.to_string(), username: user.username.to_string(), status: "Active".to_string(), }, server_info: XtreamServerInfo { - url: server.ip.to_string(), - port: server.http_port, - https_port: server.https_port, - server_protocol: server.protocol.clone(), - rtmp_port: server.rtmp_port, - timezone: server.timezone.to_string(), + url: server_info.host.to_owned(), + port: server_info.http_port.to_owned(), + https_port: server_info.https_port.to_owned(), + server_protocol: server_info.protocol.clone(), + rtmp_port: server_info.rtmp_port.to_owned(), + timezone: server_info.timezone.to_string(), timestamp_now: now.timestamp(), time_now: now.format("%Y-%m-%d %H:%M:%S").to_string(), }, diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index 46265fcbc..e732910d9 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -50,7 +50,8 @@ pub(crate) struct UserCredentials { pub password: String, pub token: Option, #[serde(default = "ProxyType::default")] - pub proxy: ProxyType + pub proxy: ProxyType, + pub server: Option, } impl UserCredentials { @@ -100,8 +101,9 @@ fn default_as_1935() -> String { "1935".to_string() } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ApiProxyServerInfo { + pub name: String, pub protocol: String, - pub ip: String, + pub host: String, pub http_port: String, #[serde(default = "default_as_443")] pub https_port: String, @@ -117,8 +119,8 @@ impl ApiProxyServerInfo { if self.protocol.is_empty() { return false; } - self.ip = self.ip.trim().to_string(); - if self.ip.is_empty() { + self.host = self.host.trim().to_string(); + if self.host.is_empty() { return false; } self.http_port = self.http_port.trim().to_string(); @@ -153,7 +155,7 @@ impl ApiProxyServerInfo { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ApiProxyConfig { - pub server: ApiProxyServerInfo, + pub server: Vec, pub user: Vec, #[serde(skip_serializing, skip_deserializing)] pub _file_path: String, @@ -164,6 +166,20 @@ impl ApiProxyConfig { let mut usernames = HashSet::new(); let mut tokens = HashSet::new(); let mut errors = Vec::new(); + if self.server.is_empty() { + errors.push("No serverinfo defined".to_string()); + } else { + let mut name_set = HashSet::new(); + for server in &self.server { + if server.name.trim().is_empty() { + errors.push("Server info name is empty ".to_owned()); + } else if name_set.contains(server.name.as_str()) { + errors.push(format!("Non unique server info name found {}", &server.name)); + } else { + name_set.insert(server.name.clone()); + } + } + } for target_user in &mut self.user { for user in &mut target_user.credentials { if usernames.contains(&user.username) { @@ -180,6 +196,12 @@ impl ApiProxyConfig { tokens.insert(token.to_string()); } } + + if let Some(server_info_name) = &user.server { + if ! &self.server.iter().any(|server_info| server_info.name.eq(server_info_name)) { + errors.push(format!("No server info with name {} found for user {}", server_info_name, &user.username)); + } + } } } if errors.is_empty() {