Added multiple server definition for api-proxy configuration

This commit is contained in:
euzu
2024-01-29 12:10:29 +01:00
parent 733d9a3d2a
commit b2a23d9799
15 changed files with 162 additions and 57 deletions
+6 -1
View File
@@ -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
+26 -8
View File
@@ -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}
```
+18 -8
View File
@@ -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}
- {username: x3452, password: ztrhgrGZrt83hjerter, token: 4243234wer234er234, proxy: redirect, server: default}
- {username: x3453, password: kljlkjorn87jfkhksdh, token: 43dfgerrt345345ss4, proxy: reverse, server: extern}
+2 -2
View File
@@ -14,7 +14,7 @@ export default interface ConfigApiService extends ApiService {
saveTargetUser(targetUser: TargetUser[]): Observable<any>;
saveApiProxyConfig(serverInfo: ApiProxyServerInfo): Observable<any>;
saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]): Observable<any>;
}
export class DefaultConfigApiService extends DefaultApiService implements ConfigApiService {
@@ -30,7 +30,7 @@ export class DefaultConfigApiService extends DefaultApiService implements Config
return this.post<ServerConfig>(SERVER_CONFIG_TARGET_USER_API_PATH, targetUser);
}
saveApiProxyConfig(serverInfo: ApiProxyServerInfo): Observable<any> {
saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]): Observable<any> {
return this.post<ServerConfig>(SERVER_CONFIG_SERVER_INFO_API_PATH, serverInfo);
}
@@ -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;
@@ -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<ApiProxyServerInfo>(() => config?.api_proxy?.server, [config]);
const serverInfo = useMemo<ApiProxyServerInfo[]>(() => config?.api_proxy?.server, [config]);
const handleSave = useCallback(() => {
if (serverInfo) {
@@ -46,7 +47,12 @@ export default function ApiProxyView(props: ApiProxyViewProps) {
<button title={'Save'} onClick={handleSave}>Save</button>
</div>
<div className={'api-proxy__content'}>
<FormView data={serverInfo} fields={SERVER_INFO_FIELDS}></FormView>
<div className={'api-proxy__content-area'}>
{serverInfo?.map(server => <div className={"card"}>
<FormView data={server} fields={SERVER_INFO_FIELDS}></FormView>
</div>)
}
</div>
</div>
<div className="api-proxy__content-help">
<span className="api-proxy__content-help-warn-icon">{getIconByName('Warn')}</span>
@@ -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 <span>{data?.[field.name]}</span>;
case FormFieldType.CHECK:
return <Checkbox label={undefined} value={field.name} checked={data?.[field.name]} onSelect={handleCheckboxChange}></Checkbox>
case FormFieldType.MULTI_SELECT:
@@ -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 {
+17 -6
View File
@@ -25,9 +25,12 @@ export default function UserView(props: UserViewProps) {
const [targets, setTargets] = useState<TargetUser[]>([]);
const [activeTarget, setActiveTarget] = useState<string>(undefined);
const [tabs, setTabs] = useState<TabSetTab[]>([]);
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) {
<label>Password</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>Token</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>Server</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>Proxy</label></div>
<div className={'user__target-user-col user__target-user-col-header'}></div>
@@ -193,13 +199,18 @@ export default function UserView(props: UserViewProps) {
data-field={field} onChange={handleValueChange}></input>
</div>
)}
<div key={'target_' + target.target + '_server_' + usr.username}
className={'user__target-user-col '}>
<div className={'user__target-user-col-label'}><label>Proxy</label></div>
<TagSelect options={serverOptions} name={target.target + '@' + idx + '@server'}
defaultValues={(usr as any)?.['server']} radio={true} multi={false} onSelect={handleChange}></TagSelect>
</div>
<div key={'target_' + target.target + '_proxy_' + usr.username}
className={'user__target-user-col '}>
<div className={'user__target-user-col-label'}><label>Proxy</label></div>
<TagSelect options={PROXY_OPTIONS} name={target.target + '-' + idx}
<TagSelect options={PROXY_OPTIONS} name={target.target + '@' + idx + '@proxy'}
defaultValues={(usr as any)?.['proxy']} radio={true} multi={false} onSelect={handleChange}></TagSelect>
</div>
<div className={'user__target-user-col user__target-user-col-toolbar'}>
<span data-target={target.target} data-idx={idx} onClick={handleUserRemove}>
{getIconByName('PersonRemove')}
+1 -1
View File
@@ -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/',
}
};
+3 -2
View File
@@ -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[];
}
+1 -1
View File
@@ -18,7 +18,7 @@ export default class ConfigService {
return this.configApiService.saveTargetUser(targetUser);
}
saveApiProxyConfig(serverInfo: ApiProxyServerInfo) {
saveApiProxyConfig(serverInfo: ApiProxyServerInfo[]) {
return this.configApiService.saveApiProxyConfig(serverInfo);
}
}
+13 -11
View File
@@ -67,21 +67,23 @@ pub(crate) async fn save_config_main(
pub(crate) async fn save_config_api_proxy_config(
mut req: web::Json<ApiProxyServerInfo>,
req: web::Json<Vec<ApiProxyServerInfo>>,
mut _app_state: web::Data<AppState>,
) -> 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(
+17 -8
View File
@@ -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(),
},
+27 -5
View File
@@ -50,7 +50,8 @@ pub(crate) struct UserCredentials {
pub password: String,
pub token: Option<String>,
#[serde(default = "ProxyType::default")]
pub proxy: ProxyType
pub proxy: ProxyType,
pub server: Option<String>,
}
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<ApiProxyServerInfo>,
pub user: Vec<TargetUser>,
#[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() {