mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-04 15:02:16 +02:00
Added multiple server definition for api-proxy configuration
This commit is contained in:
+6
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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/',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user